From 3086b0e9b4df4b24feee6a2ace04cc6f1a4493b5 Mon Sep 17 00:00:00 2001 From: mStar <12024604-mstarongitlab@users.noreply.gitlab.com> Date: Fri, 31 May 2024 11:54:39 +0200 Subject: [PATCH] Lots of work --- ap/checkHeader.go | 24 ++++ ap/common_types.go | 17 +++ ap/constants.go | 58 ++++++++ ap/context.go | 49 +++++++ ap/examples/masto_account.json | 185 ++++++++++++++++++++++++++ ap/examples/masto_follow_request.json | 18 +++ ap/examples/mk_note.json | 41 ++++++ ap/examples/mk_note_create.json | 70 ++++++++++ ap/explicit_types.go | 62 +++++++++ ap/parser.go | 59 ++++++++ ap/type_parsers.go | 172 ++++++++++++++++++++++++ go.mod | 35 ++++- go.sum | 78 +++++++++++ server/endpoints_ap.go | 36 +++++ server/middlewares/injectLogrus.go | 20 +++ server/middlewares/injectStorage.go | 22 +++ server/middlewares/middleswares.go | 14 ++ server/notes.go | 23 ++++ server/server.go | 27 ++++ storage/mediaFile.go | 22 +++ storage/noteTargets.go | 28 ++++ storage/notes.go | 28 ++++ storage/remoteServerInfo.go | 18 +++ storage/serverTypes.go | 33 +++++ storage/storage.go | 36 +++++ storage/user.go | 67 ++++++++++ storage/userIdentType.go | 30 +++++ storage/userInfoFields.go | 14 ++ 28 files changed, 1284 insertions(+), 2 deletions(-) create mode 100644 ap/checkHeader.go create mode 100644 ap/common_types.go create mode 100644 ap/constants.go create mode 100644 ap/context.go create mode 100644 ap/examples/masto_account.json create mode 100644 ap/examples/masto_follow_request.json create mode 100644 ap/examples/mk_note.json create mode 100644 ap/examples/mk_note_create.json create mode 100644 ap/explicit_types.go create mode 100644 ap/parser.go create mode 100644 ap/type_parsers.go create mode 100644 server/endpoints_ap.go create mode 100644 server/middlewares/injectLogrus.go create mode 100644 server/middlewares/injectStorage.go create mode 100644 server/middlewares/middleswares.go create mode 100644 server/notes.go create mode 100644 server/server.go create mode 100644 storage/mediaFile.go create mode 100644 storage/noteTargets.go create mode 100644 storage/notes.go create mode 100644 storage/remoteServerInfo.go create mode 100644 storage/serverTypes.go create mode 100644 storage/storage.go create mode 100644 storage/user.go create mode 100644 storage/userIdentType.go create mode 100644 storage/userInfoFields.go diff --git a/ap/checkHeader.go b/ap/checkHeader.go new file mode 100644 index 0000000..5c09332 --- /dev/null +++ b/ap/checkHeader.go @@ -0,0 +1,24 @@ +package ap + +import "strings" + +// Header used for making requests for AP resources +// Not used yet +const OUTBOUND_REQUEST_HEADER = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1" + +var contentHeadersToCheck = []string{ + "application/json", + "application/ld+json", +} + +// Check a given string if it contains any of the content types specified in +// the contentHeadersToCheck slice +// Used for differentiating requests for the ActivityPub version of some data vs frontend version +func ContainsApContentHeader(toCheck string) bool { + for _, h := range contentHeadersToCheck { + if strings.Contains(toCheck, h) { + return true + } + } + return false +} diff --git a/ap/common_types.go b/ap/common_types.go new file mode 100644 index 0000000..0d70934 --- /dev/null +++ b/ap/common_types.go @@ -0,0 +1,17 @@ +package ap + +import "net/url" + +type IdType struct { + Id *url.URL +} + +type ValueType[T any] struct { + Value T +} + +type Media struct { + Type url.URL + MediaType string + Url url.URL +} diff --git a/ap/constants.go b/ap/constants.go new file mode 100644 index 0000000..9e25637 --- /dev/null +++ b/ap/constants.go @@ -0,0 +1,58 @@ +package ap + +const ( + KEY_ID = "@id" // Value of type string + KEY_TYPE = "@type" // Value of type string slice / activitystreams object url slice + KEY_VALUE = "@value" // Could be any type really +) + +const ( + KEY_MASTO_DEVICES = "http://joinmastodon.org/ns#devices" + KEY_MASTO_DISCOVERABLE = "http://joinmastodon.org/ns#discoverable" + KEY_MASTO_FEATURED = "http://joinmastodon.org/ns#featured" + KEY_MASTO_FEATURED_TAGS = "http://joinmastodon.org/ns#featuredTags" + KEY_MASTO_INDEXABLE = "http://joinmastodon.org/ns#indexable" + KEY_MASTO_MEMORIAL = "http://joinmastodon.org/ns#memorial" +) + +const ( + KEY_W3_INBOX = "http://www.w3.org/ns/ldp#inbox" + KEY_W3_SECURITY_PUBLICKEY = "https://w3id.org/security#publicKey" + KEY_W3_SECURITY_OWNER = "https://w3id.org/security#owner" + KEY_W3_SECURITY_PUBLICKEYPEM = "https://w3id.org/security#publicKeyPem" +) + +const ( + KEY_ACTIVITYSTREAMS_ALSOKNOWNAS = "https://www.w3.org/ns/activitystreams#alsoKnownAs" + KEY_ACTIVITYSTREAMS_ATTACHMENTS = "https://www.w3.org/ns/activitystreams#attachment" + KEY_ACTIVITYSTREAMS_NAME = "https://www.w3.org/ns/activitystreams#name" + KEY_ACTIVITYSTREAMS_ENDPOINTS = "https://www.w3.org/ns/activitystreams#endpoints" + KEY_ACTIVITYSTREAMS_SHAREDINBOX = "https://www.w3.org/ns/activitystreams#sharedInbox" + KEY_ACTIVITYSTREAMS_FOLLOWERS = "https://www.w3.org/ns/activitystreams#followers" + KEY_ACTIVITYSTREAMS_FOLLOWING = "https://www.w3.org/ns/activitystreams#following" + KEY_ACTIVITYSTREAMS_ICON = "https://www.w3.org/ns/activitystreams#icon" + KEY_ACTIVITYSTREAMS_MEDIATYPE = "https://www.w3.org/ns/activitystreams#mediaType" + KEY_ACTIVITYSTREAMS_URL = "https://www.w3.org/ns/activitystreams#url" + KEY_ACTIVITYSTREAMS_IMAGE = "https://www.w3.org/ns/activitystreams#image" + KEY_ACTIVITYSTREAMS_RESTRICTED_FOLLOW = "https://www.w3.org/ns/activitystreams#manuallyApprovesFollowers" + KEY_ACTIVITYSTREAMS_OUTBOX = "https://www.w3.org/ns/activitystreams#outbox" + KEY_ACTIVITYSTREAMS_PREFFEREDUSERNAME = "https://www.w3.org/ns/activitystreams#preferredUsername" + KEY_ACTIVITYSTREAMS_PUBLISHED = "https://www.w3.org/ns/activitystreams#published" + KEY_ACTIVITYSTREAMS_SUMMARY = "https://www.w3.org/ns/activitystreams#summary" + KEY_ACTIVITYSTREAMS_TAG = "https://www.w3.org/ns/activitystreams#tag" + KEY_ACTIVITYSTREAMS_CC = "https://www.w3.org/ns/activitystreams#cc" + KEY_ACTIVITYSTREAMS_TO = "https://www.w3.org/ns/activitystreams#to" + KEY_ACTIVITYSTREAMS_OBJECT = "https://www.w3.org/ns/activitystreams#object" + + // Object types (I think) + // Those are values the object type can have + + KEY_ACTIVITYSTREAMS_ACTOR = "https://www.w3.org/ns/activitystreams#actor" + KEY_ACTIVITYSTREAMS_FOLLOW = "https://www.w3.org/ns/activitystreams#Follow" + KEY_ACTIVITYSTREAMS_PERSON = "https://www.w3.org/ns/activitystreams#Person" + KEY_ACTIVITYSTREAMS_CREATE = "https://www.w3.org/ns/activitystreams#Create" +) + +const ( + KEY_SCHEMA_VALUE = "http://schema.org#value" +) diff --git a/ap/context.go b/ap/context.go new file mode 100644 index 0000000..71f4a4f --- /dev/null +++ b/ap/context.go @@ -0,0 +1,49 @@ +package ap + +// Just steal this one from sharkey +// Also use this one for all AP objects for now +// Can make a function to extract context from an expanded object later +// TODO: Consider if such a function is worth it or if it would hinder learning +var allContext = map[string]any{ + "@context": []any{ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + map[string]any{ + "fedibird": "http://fedibird.com/ns#", + "toot": "http://joinmastodon.org/ns#", + "schema": "http://schema.org#", + "misskey": "https://misskey-hub.net/ns#", + "firefish": "https://joinfirefish.org/ns#", + "sharkey": "https://joinsharkey.org/ns#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + + "Key": "sec:Key", + + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + + "quoteUri": "fedibird:quoteUri", + + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_summary": "misskey:_misskey_summary", + "isCat": "misskey:isCat", + + "speakAsCat": "firefish:speakAsCat", + + "backgroundUrl": "sharkey:backgroundUrl", + "listenbrainz": "sharkey:listenbrainz", + }, + }, +} diff --git a/ap/examples/masto_account.json b/ap/examples/masto_account.json new file mode 100644 index 0000000..3f739b0 --- /dev/null +++ b/ap/examples/masto_account.json @@ -0,0 +1,185 @@ +[ + { + "@id": "https://mastodon.social/users/Gargron", + "@type": [ + "https://www.w3.org/ns/activitystreams#Person" + ], + "http://joinmastodon.org/ns#devices": [ + { + "@id": "https://mastodon.social/users/Gargron/collections/devices" + } + ], + "http://joinmastodon.org/ns#discoverable": [ + { + "@value": true + } + ], + "http://joinmastodon.org/ns#featured": [ + { + "@id": "https://mastodon.social/users/Gargron/collections/featured" + } + ], + "http://joinmastodon.org/ns#featuredTags": [ + { + "@id": "https://mastodon.social/users/Gargron/collections/tags" + } + ], + "http://joinmastodon.org/ns#indexable": [ + { + "@value": true + } + ], + "http://joinmastodon.org/ns#memorial": [ + { + "@value": false + } + ], + "http://www.w3.org/ns/ldp#inbox": [ + { + "@id": "https://mastodon.social/users/Gargron/inbox" + } + ], + "https://w3id.org/security#publicKey": [ + { + "@id": "https://mastodon.social/users/Gargron#main-key", + "https://w3id.org/security#owner": [ + { + "@id": "https://mastodon.social/users/Gargron" + } + ], + "https://w3id.org/security#publicKeyPem": [ + { + "@value": "-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXc4vkECU2/CeuSo1wtn Foim94Ne1jBMYxTZ9wm2YTdJq1oiZKif06I2fOqDzY/4q/S9uccrE9Bkajv1dnkO Vm31QjWlhVpSKynVxEWjVBO5Ienue8gND0xvHIuXf87o61poqjEoepvsQFElA5ym ovljWGSA/jpj7ozygUZhCXtaS2W5AD5tnBQUpcO0lhItYPYTjnmzcc4y2NbJV8hz 2s2G8qKv8fyimE23gY1XrPJg+cRF+g4PqFXujjlJ7MihD9oqtLGxbu7o1cifTn3x BfIdPythWu5b4cujNsB3m3awJjVmx+MHQ9SugkSIYXV0Ina77cTNS0M2PYiH1PFR TwIDAQAB -----END PUBLIC KEY----- " + } + ] + } + ], + "https://www.w3.org/ns/activitystreams#alsoKnownAs": [ + { + "@id": "https://tooting.ai/users/Gargron" + } + ], + "https://www.w3.org/ns/activitystreams#attachment": [ + { + "@type": [ + "http://schema.org#PropertyValue" + ], + "http://schema.org#value": [ + { + "@value": "\u003ca href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" + } + ], + "https://www.w3.org/ns/activitystreams#name": [ + { + "@value": "Patreon" + } + ] + }, + { + "@type": [ + "http://schema.org#PropertyValue" + ], + "http://schema.org#value": [ + { + "@value": "\u003ca href=\"https://github.com/Gargron\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/Gargron\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" + } + ], + "https://www.w3.org/ns/activitystreams#name": [ + { + "@value": "GitHub" + } + ] + } + ], + "https://www.w3.org/ns/activitystreams#endpoints": [ + { + "https://www.w3.org/ns/activitystreams#sharedInbox": [ + { + "@id": "https://mastodon.social/inbox" + } + ] + } + ], + "https://www.w3.org/ns/activitystreams#followers": [ + { + "@id": "https://mastodon.social/users/Gargron/followers" + } + ], + "https://www.w3.org/ns/activitystreams#following": [ + { + "@id": "https://mastodon.social/users/Gargron/following" + } + ], + "https://www.w3.org/ns/activitystreams#icon": [ + { + "@type": [ + "https://www.w3.org/ns/activitystreams#Image" + ], + "https://www.w3.org/ns/activitystreams#mediaType": [ + { + "@value": "image/png" + } + ], + "https://www.w3.org/ns/activitystreams#url": [ + { + "@id": "https://files.mastodon.social/accounts/avatars/000/000/001/original/a0a49d80c3de5f75.png" + } + ] + } + ], + "https://www.w3.org/ns/activitystreams#image": [ + { + "@type": [ + "https://www.w3.org/ns/activitystreams#Image" + ], + "https://www.w3.org/ns/activitystreams#mediaType": [ + { + "@value": "image/jpeg" + } + ], + "https://www.w3.org/ns/activitystreams#url": [ + { + "@id": "https://files.mastodon.social/accounts/headers/000/000/001/original/d13e4417706a5fec.jpg" + } + ] + } + ], + "https://www.w3.org/ns/activitystreams#manuallyApprovesFollowers": [ + { + "@value": false + } + ], + "https://www.w3.org/ns/activitystreams#name": [ + { + "@value": "Eugen Rochko" + } + ], + "https://www.w3.org/ns/activitystreams#outbox": [ + { + "@id": "https://mastodon.social/users/Gargron/outbox" + } + ], + "https://www.w3.org/ns/activitystreams#preferredUsername": [ + { + "@value": "Gargron" + } + ], + "https://www.w3.org/ns/activitystreams#published": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2016-03-16T00:00:00Z" + } + ], + "https://www.w3.org/ns/activitystreams#summary": [ + { + "@value": "\u003cp\u003eFounder of \u003cspan class=\"h-card\" translate=\"no\"\u003e\u003ca href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\"\u003e@\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e. Film photography, prog metal, Dota 2. Likes all things analog.\u003c/p\u003e" + } + ], + "https://www.w3.org/ns/activitystreams#tag": [], + "https://www.w3.org/ns/activitystreams#url": [ + { + "@id": "https://mastodon.social/@Gargron" + } + ] + } +] diff --git a/ap/examples/masto_follow_request.json b/ap/examples/masto_follow_request.json new file mode 100644 index 0000000..d483b73 --- /dev/null +++ b/ap/examples/masto_follow_request.json @@ -0,0 +1,18 @@ +[ + { + "@id": "https://activitypub.academy/1e8a5594-eff7-4946-86fe-84d82d0a14ae", + "@type": [ + "https://www.w3.org/ns/activitystreams#Follow" + ], + "https://www.w3.org/ns/activitystreams#actor": [ + { + "@id": "https://activitypub.academy/users/dadacio_dashdorrol" + } + ], + "https://www.w3.org/ns/activitystreams#object": [ + { + "@id": "https://woem.men/users/9n39zo1rfckr00q5" + } + ] + } +] diff --git a/ap/examples/mk_note.json b/ap/examples/mk_note.json new file mode 100644 index 0000000..7376e7d --- /dev/null +++ b/ap/examples/mk_note.json @@ -0,0 +1,41 @@ +[ + { + "@id": "https://woem.men/notes/9ttp29lhge2u0454", + "@type": [ + "https://www.w3.org/ns/activitystreams#Note" + ], + "https://www.w3.org/ns/activitystreams#attachment": [], + "https://www.w3.org/ns/activitystreams#attributedTo": [ + { + "@id": "https://woem.men/users/9n39zo1rfckr00q5" + } + ], + "https://www.w3.org/ns/activitystreams#cc": [ + { + "@id": "https://woem.men/users/9n39zo1rfckr00q5/followers" + } + ], + "https://www.w3.org/ns/activitystreams#content": [ + { + "@value": "\u003cp\u003eTest post, ignore\u003c/p\u003e" + } + ], + "https://www.w3.org/ns/activitystreams#published": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-05-28T08:22:59.861Z" + } + ], + "https://www.w3.org/ns/activitystreams#sensitive": [ + { + "@value": false + } + ], + "https://www.w3.org/ns/activitystreams#tag": [], + "https://www.w3.org/ns/activitystreams#to": [ + { + "@id": "https://www.w3.org/ns/activitystreams#Public" + } + ] + } +] diff --git a/ap/examples/mk_note_create.json b/ap/examples/mk_note_create.json new file mode 100644 index 0000000..fce13ff --- /dev/null +++ b/ap/examples/mk_note_create.json @@ -0,0 +1,70 @@ +[ + { + "@id": "https://woem.men/notes/9ttp29lhge2u0454/activity", + "@type": [ + "https://www.w3.org/ns/activitystreams#Create" + ], + "https://www.w3.org/ns/activitystreams#actor": [ + { + "@id": "https://woem.men/users/9n39zo1rfckr00q5" + } + ], + "https://www.w3.org/ns/activitystreams#cc": [ + { + "@id": "https://woem.men/users/9n39zo1rfckr00q5/followers" + } + ], + "https://www.w3.org/ns/activitystreams#object": [ + { + "@id": "https://woem.men/notes/9ttp29lhge2u0454", + "@type": [ + "https://www.w3.org/ns/activitystreams#Note" + ], + "https://www.w3.org/ns/activitystreams#attachment": [], + "https://www.w3.org/ns/activitystreams#attributedTo": [ + { + "@id": "https://woem.men/users/9n39zo1rfckr00q5" + } + ], + "https://www.w3.org/ns/activitystreams#cc": [ + { + "@id": "https://woem.men/users/9n39zo1rfckr00q5/followers" + } + ], + "https://www.w3.org/ns/activitystreams#content": [ + { + "@value": "\u003cp\u003eTest post, ignore\u003c/p\u003e" + } + ], + "https://www.w3.org/ns/activitystreams#published": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-05-28T08:22:59.861Z" + } + ], + "https://www.w3.org/ns/activitystreams#sensitive": [ + { + "@value": false + } + ], + "https://www.w3.org/ns/activitystreams#tag": [], + "https://www.w3.org/ns/activitystreams#to": [ + { + "@id": "https://www.w3.org/ns/activitystreams#Public" + } + ] + } + ], + "https://www.w3.org/ns/activitystreams#published": [ + { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2024-05-28T08:22:59.861Z" + } + ], + "https://www.w3.org/ns/activitystreams#to": [ + { + "@id": "https://www.w3.org/ns/activitystreams#Public" + } + ] + } +] diff --git a/ap/explicit_types.go b/ap/explicit_types.go new file mode 100644 index 0000000..0617363 --- /dev/null +++ b/ap/explicit_types.go @@ -0,0 +1,62 @@ +package ap + +import ( + "net/url" + "time" +) + +type ( + ActivityStreamsAlsoKnownAs IdType + ActivityStreamsAttachment struct { + Type *url.URL + Value string + Name string + } + ActivityStreamsSharedInbox IdType + ActivityStreamsFollowers IdType + ActivityStreamsFollowing IdType + ActivityStreamsImage Media + ActivityStreamsIcon Media + ActivityStreamsRestrictedFollow ValueType[bool] + ActivityStreamsName ValueType[string] + ActivityStreamsOutbox IdType + ActivityStreamsPrefferedUsername ValueType[string] + ActivityStreamsPublished struct { + Type string + Value time.Time + } + ActivityStreamsSummary ValueType[string] + ActivityStreamsUrl IdType + // NOTE: I do not know if this is consistent at all. Do not trust yet + ActivityStreamsTag struct { + Type string + Id string + Name string + } + ActivityStreamsTo IdType + ActivityStreamsCC IdType + + ActivityStreamsActor IdType + // NOTE: Technically, objects can have a LOT of data. I don't care. Treat them as ID type + // Just fetch whatever the ID is later on separately and throw away anything else but the ID + ActivityStreamsObject IdType + ActivityStreamsFollow IdType +) + +type ( + MastoDevices IdType + MastoDiscoverable ValueType[bool] + MastoFeatured IdType + MastoFeaturedTags IdType + MastoIndexable ValueType[bool] + MastoMemorial ValueType[bool] +) + +type ( + W3Inbox IdType + W3SecurityPublicKey struct { + Id *url.URL + Owner *url.URL + KeyPem string + } +) diff --git a/ap/parser.go b/ap/parser.go new file mode 100644 index 0000000..f91d631 --- /dev/null +++ b/ap/parser.go @@ -0,0 +1,59 @@ +package ap + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/piprate/json-gold/ld" +) + +type ApThing map[string]any + +type RemoteDocumentLoader struct{} + +// Try and parse a remote ActivityPub object into a more usable form +// Result is a map[string]any where the keys defined in /ap/constants.go should be usable, +// depending on the type of object +// The general approach for fetching an object will be to fetch the main object +// and only store the ID for sub-objects to fetch them later +func ParseFromUrl(u *url.URL) (ApThing, error) { + opts := ld.NewJsonLdOptions("") + processor := ld.NewJsonLdProcessor() + // TODO: Add custom document parser (copy default implementation) that includes verification + + // Explanation: + // Expansion removes the context from a document (json-ld activitypub data) + // and turns every field into something along the lines of + // "https://example.com/ns#ObjectType": + // This makes it easier to deal with things as they now have a very consistent naming scheme + // See /ap/constants.go for those + + parsed, err := processor.Expand(u.String(), opts) + if err != nil { + return nil, fmt.Errorf("failed to process remote document: %w", err) + } + if len(parsed) == 0 { + return nil, fmt.Errorf("document has a length of 0") + } + typed, ok := parsed[0].(ApThing) + if !ok { + return nil, fmt.Errorf("couldn't cast data to ApThing") + } + return typed, nil +} + +// Compact an AP object down into a compressed json-ld form +// That compacted form should be accepted by all AP servers +// It also handles context for any fields +// Content should only use keys defined in /ap/constants.go though +// Other things might get lost in translation +func Compact(content map[string]any) ([]byte, error) { + opts := ld.NewJsonLdOptions("") + processor := ld.NewJsonLdProcessor() + res, err := processor.Compact(content, allContext, opts) + if err != nil { + return nil, fmt.Errorf("failed to compact data: %w", err) + } + return json.Marshal(res) +} diff --git a/ap/type_parsers.go b/ap/type_parsers.go new file mode 100644 index 0000000..451f099 --- /dev/null +++ b/ap/type_parsers.go @@ -0,0 +1,172 @@ +package ap + +import ( + "net/url" + "time" +) + +// Try and parse a value into an IdType +// Returns nil if failed +func TryParseIdType(rawIn any) *IdType { + switch in := rawIn.(type) { + case []any: + if len(in) == 0 { + return nil + } + m, ok := in[0].(map[string]any) + if !ok { + return nil + } + return TryParseIdType(m) + case map[string]any: + vRaw, ok := in[KEY_ID] + if !ok { + return nil + } + v, ok := vRaw.(string) + if !ok { + return nil + } + u, err := url.Parse(v) + if err != nil { + return nil + } + return &IdType{ + Id: u, + } + default: + return nil + } +} + +func TryParseValueType[T any](rawIn any) *ValueType[T] { + switch in := rawIn.(type) { + case map[string]any: + vRaw, ok := in[KEY_ID] + if !ok { + return nil + } + v, ok := vRaw.(T) + if !ok { + return nil + } + return &ValueType[T]{ + Value: v, + } + case []any: + if len(in) == 0 { + return nil + } + v, ok := in[0].(map[string]any) + if !ok { + return nil + } + return TryParseValueType[T](v) + default: + return nil + } +} + +func TryParseActivityStreamsPublicKey(rawIn any) *W3SecurityPublicKey { + switch in := rawIn.(type) { + case map[string]any: + asIdType := TryParseIdType(in) + if asIdType == nil { + return nil + } + ownerType := TryParseIdType(in[KEY_W3_SECURITY_OWNER]) + if ownerType == nil { + return nil + } + keyValue := TryParseValueType[string](in[KEY_W3_SECURITY_PUBLICKEYPEM]) + if keyValue == nil { + return nil + } + return &W3SecurityPublicKey{ + Id: asIdType.Id, + Owner: ownerType.Id, + KeyPem: keyValue.Value, + } + case []any: + if len(in) == 0 { + return nil + } + return TryParseActivityStreamsPublicKey(in[0]) + default: + return nil + } +} + +func TryParseActivityStreamsAttachment(rawIn any) *ActivityStreamsAttachment { + switch in := rawIn.(type) { + case []any: + if len(in) == 0 { + return nil + } + return TryParseActivityStreamsAttachment(in[0]) + case map[string]any: + rawType, ok := in[KEY_TYPE] + if !ok { + return nil + } + strType, ok := rawType.(string) + if !ok { + return nil + } + urlType, err := url.Parse(strType) + if err != nil { + return nil + } + value := TryParseValueType[string](in[KEY_SCHEMA_VALUE]) + if value == nil { + return nil + } + name := TryParseValueType[string](in[KEY_ACTIVITYSTREAMS_NAME]) + if name == nil { + return nil + } + return &ActivityStreamsAttachment{ + Type: urlType, + Name: name.Value, + Value: value.Value, + } + default: + return nil + } +} + +func TryParseActivityStreamsPublished(rawIn any) *ActivityStreamsPublished { + switch in := rawIn.(type) { + case []any: + if len(in) == 0 { + return nil + } + return TryParseActivityStreamsPublished(in[0]) + case map[string]any: + rawType, ok := in[KEY_TYPE] + if !ok { + return nil + } + strType, ok := rawType.(string) + if !ok { + return nil + } + value := TryParseValueType[string](in) + tv, err := time.Parse("2006-01-02T04:05:06Z", value.Value) + if err != nil { + return nil + } + return &ActivityStreamsPublished{ + Type: strType, + Value: tv, + } + default: + return nil + } +} + +// NOTE: Since I do not know if tags are consistent with the struct yet, +// This funtion does not do anything yet and should not be used +func TryParseActivityStreamsTag(rawIn any) *ActivityStreamsTag { + return nil +} diff --git a/go.mod b/go.mod index 611cf1a..a168303 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,37 @@ module gitlab.com/mstarongitlab/linstrom go 1.22.2 require ( - github.com/piprate/json-gold v0.5.0 // indirect - github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/piprate/json-gold v0.5.0 + github.com/sirupsen/logrus v1.9.3 + gorm.io/gorm v1.25.10 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect + github.com/go-webauthn/webauthn v0.10.2 // indirect + github.com/go-webauthn/x v0.1.9 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/go-tpm v0.9.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + gorm.io/driver/postgres v1.5.7 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/go.sum b/go.sum index a726309..7a158cb 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,82 @@ +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4= +github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs= +github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE= +github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= +github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/piprate/json-gold v0.5.0 h1:RmGh1PYboCFcchVFuh2pbSWAZy4XJaqTMU4KQYsApbM= github.com/piprate/json-gold v0.5.0/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs= +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/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/server/endpoints_ap.go b/server/endpoints_ap.go new file mode 100644 index 0000000..ce37bb1 --- /dev/null +++ b/server/endpoints_ap.go @@ -0,0 +1,36 @@ +package server + +import ( + "fmt" + "net/http" + "strings" + + "github.com/sirupsen/logrus" + "gitlab.com/mstarongitlab/linstrom/server/middlewares" + "gitlab.com/mstarongitlab/linstrom/storage" +) + +// Mount under /.well-known/webfinger +func webfingerHandler(w http.ResponseWriter, r *http.Request) { + logEntry, ok := r.Context().Value(middlewares.CONTEXT_KEY_LOGRUS).(*logrus.Entry) + if !ok { + http.Error(w, "couldn't get logging entry from context", http.StatusInternalServerError) + return + } + store := storage.Storage{} + + requestedResource := r.FormValue("resource") + if requestedResource == "" { + http.Error(w, "bad request. Include \"resource\" parameter", http.StatusBadRequest) + logEntry.Infoln("No resource parameter. Cancelling") + return + } + accName := strings.TrimPrefix(requestedResource, "acc:") + acc, err := store.FindLocalAccount(accName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logEntry.WithError(err).Warningln("couldn't find account") + return + } + fmt.Fprint(w, acc) +} diff --git a/server/middlewares/injectLogrus.go b/server/middlewares/injectLogrus.go new file mode 100644 index 0000000..af68322 --- /dev/null +++ b/server/middlewares/injectLogrus.go @@ -0,0 +1,20 @@ +package middlewares + +import ( + "context" + "net/http" + + "github.com/sirupsen/logrus" +) + +const CONTEXT_KEY_LOGRUS = ContextKey("logrus") + +// Inject a logrus entry into the context that has the url path already set +func InjectLogrusMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqContext := r.Context() + entry := logrus.WithField("url-path", r.URL.Path) + newContext := context.WithValue(reqContext, CONTEXT_KEY_LOGRUS, entry) + next.ServeHTTP(w, r.WithContext(newContext)) + }) +} diff --git a/server/middlewares/injectStorage.go b/server/middlewares/injectStorage.go new file mode 100644 index 0000000..ef5b374 --- /dev/null +++ b/server/middlewares/injectStorage.go @@ -0,0 +1,22 @@ +package middlewares + +import ( + "context" + "net/http" + + "gitlab.com/mstarongitlab/linstrom/storage" +) + +const CONTEXT_KEY_STORAGE = ContextKey("storage") + +// Build a middleware for injecting a storage reference into the context +func InjectStorageMiddlewareBuilder(store *storage.Storage) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqContext := r.Context() + newContext := context.WithValue(reqContext, CONTEXT_KEY_STORAGE, store) + newRequest := r.WithContext(newContext) + h.ServeHTTP(w, newRequest) + }) + } +} diff --git a/server/middlewares/middleswares.go b/server/middlewares/middleswares.go new file mode 100644 index 0000000..def18fe --- /dev/null +++ b/server/middlewares/middleswares.go @@ -0,0 +1,14 @@ +package middlewares + +import "net/http" + +type ContextKey string +type MiddlewareFunc func(http.Handler) http.Handler + +func ChainMiddlewares(start http.Handler, middlewares ...MiddlewareFunc) http.Handler { + next := start + for _, h := range middlewares { + next = h(next) + } + return next +} diff --git a/server/notes.go b/server/notes.go new file mode 100644 index 0000000..7304888 --- /dev/null +++ b/server/notes.go @@ -0,0 +1,23 @@ +package server + +import ( + "net/http" + + "gitlab.com/mstarongitlab/linstrom/ap" +) + +// Mount at /notes/{note-id} +// Handles the note endpoint +// Serves the json-ld representation of a note OR the frontend view +func noteHandler(w http.ResponseWriter, r *http.Request) { + if ap.ContainsApContentHeader(r.Header.Get("Content-Type")) { + apNote(w, r) + } else { + renderNote(w, r) + } +} + +func renderNote(w http.ResponseWriter, r *http.Request) { + http.Error(w, "not implemented yet", http.StatusInternalServerError) +} +func apNote(w http.ResponseWriter, r *http.Request) {} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..46e58b5 --- /dev/null +++ b/server/server.go @@ -0,0 +1,27 @@ +package server + +import ( + "net/http" + + "gitlab.com/mstarongitlab/linstrom/server/middlewares" + "gitlab.com/mstarongitlab/linstrom/storage" +) + +type Server struct { + handler http.Handler +} + +func NewServer(store *storage.Storage) *Server { + handler := http.NewServeMux() + handler.HandleFunc("/.well-known/webfinger", webfingerHandler) + // handler.Handle("/api/", http.StripPrefix("/ap", buildApiRouter())) + + withMiddlewares := middlewares.ChainMiddlewares( + handler, + middlewares.InjectLogrusMiddleware, + middlewares.InjectStorageMiddlewareBuilder(store), + ) + return &Server{ + handler: withMiddlewares, + } +} diff --git a/storage/mediaFile.go b/storage/mediaFile.go new file mode 100644 index 0000000..d6e8f3c --- /dev/null +++ b/storage/mediaFile.go @@ -0,0 +1,22 @@ +package storage + +import ( + "time" + + "gorm.io/gorm" +) + +type MediaFile struct { + ID string `gorm:"primarykey"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` + Remote bool // whether the attachment is a remote one + Link string // url if remote attachment, identifier if local + // Whether this media has been cached locally + // Only really used for user and server icons, not attachments + // Reason: Attachments would take way to much space considering that they are often only loaded a few times at most + // And caching a file for those few times would be a waste of storage + // Caching user and server icons locally however should reduce burden on remote servers by quite a bit though + LocallyCached bool +} diff --git a/storage/noteTargets.go b/storage/noteTargets.go new file mode 100644 index 0000000..af0ed8f --- /dev/null +++ b/storage/noteTargets.go @@ -0,0 +1,28 @@ +package storage + +import ( + "database/sql/driver" + "errors" +) + +type NoteTarget uint8 + +const ( + NOTE_TARGET_PUBLIC = NoteTarget(0) + NOTE_TARGET_HOME = NoteTarget(1 << iota) + NOTE_TARGET_FOLLOWERS + NOTE_TARGET_DM +) + +func (n *NoteTarget) Value() (driver.Value, error) { + return n, nil +} + +func (n *NoteTarget) Scan(value any) error { + vBig, ok := value.(int64) + if !ok { + return errors.New("not an int64") + } + *n = NoteTarget(vBig) + return nil +} diff --git a/storage/notes.go b/storage/notes.go new file mode 100644 index 0000000..ef68a38 --- /dev/null +++ b/storage/notes.go @@ -0,0 +1,28 @@ +package storage + +import ( + "time" + + "gorm.io/gorm" +) + +type Note struct { + ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` + Creator string // Full handle of the creator, eg: @max@example.com + Remote bool // Whether the note is originally a remote one and just "cached" + // Raw content of the note. So without additional formatting applied + // Might already have formatting applied beforehand from the origin server + RawContent string + ContentWarning *string // Content warnings of the note, if it contains any + Attachments []string `gorm:"serializer:json"` // Links to attachments + Emotes []string `gorm:"serializer:json"` // Emotes used in that message + RepliesTo *string // Url of the message this replies to + Quotes *string // url of the message this note quotes + Target NoteTarget // Where to send this message to (public, home, followers, dm) + Pings []string `gorm:"serializer:json"` // Who is being tagged in this message. Also serves as DM targets + OriginServer string // Url of the origin server. Also the primary key for those + Tags []string `gorm:"serializer:json"` // Hashtags +} diff --git a/storage/remoteServerInfo.go b/storage/remoteServerInfo.go new file mode 100644 index 0000000..8c5ceea --- /dev/null +++ b/storage/remoteServerInfo.go @@ -0,0 +1,18 @@ +package storage + +import ( + "time" + + "gorm.io/gorm" +) + +type RemoteServer struct { + ID string `gorm:"primarykey"` // ID is also server url + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` + ServerType RemoteServerType // What software the server is running. Useful for formatting + Name string // What the server wants to be known as (usually same as url) + Icon string // ID of a media file + IsSelf bool // Whether this server is yours truly +} diff --git a/storage/serverTypes.go b/storage/serverTypes.go new file mode 100644 index 0000000..5c5b1f2 --- /dev/null +++ b/storage/serverTypes.go @@ -0,0 +1,33 @@ +package storage + +import ( + "database/sql/driver" + "errors" +) + +// Mostly important for rendering +type RemoteServerType string + +const ( + // Includes forks like glitch-soc, etc + REMOTE_SERVER_MASTODON = RemoteServerType("Mastodon") + // Includes forks like Ice Shrimp, Sharkey, Cutiekey, etc + REMOTE_SERVER_MISSKEY = RemoteServerType("Misskey") + // Includes Akkoma + REMOTE_SERVER_PLEMORA = RemoteServerType("Plemora") + // And of course, yours truly + REMOTE_SERVER_LINSTROM = RemoteServerType("Linstrom") +) + +func (r *RemoteServerType) Value() (driver.Value, error) { + return r, nil +} + +func (r *RemoteServerType) Scan(raw any) error { + if v, ok := raw.(string); ok { + *r = RemoteServerType(v) + return nil + } else { + return errors.New("value not a string") + } +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..636f815 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,36 @@ +package storage + +import ( + "github.com/glebarez/sqlite" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type Storage struct { + db *gorm.DB +} + +func NewStorageSqlite(filePath string) (*Storage, error) { + db, err := gorm.Open(sqlite.Open(filePath)) + if err != nil { + return nil, err + } + return &Storage{ + db: db, + }, nil +} + +func NewStoragePostgres(dbUrl string) (*Storage, error) { + db, err := gorm.Open(postgres.Open(dbUrl)) + if err != nil { + return nil, err + } + return &Storage{ + db: db, + }, nil +} + +// TODO: Placeholder. Update to proper implementation later. Including signature +func (s *Storage) FindLocalAccount(handle string) (string, error) { + return handle, nil +} diff --git a/storage/user.go b/storage/user.go new file mode 100644 index 0000000..ad4dd72 --- /dev/null +++ b/storage/user.go @@ -0,0 +1,67 @@ +package storage + +import ( + "time" + + "github.com/go-webauthn/webauthn/webauthn" + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Database representation of a user account +// This can be a bot, remote or not +// If remote, this is used for caching the account +type User struct { + ID string `gorm:"primarykey"` // ID is the full handle, eg @max@example.com + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this account was last updated. Will also be used for refreshing remote accounts + DeletedAt gorm.DeletedAt `gorm:"index"` + Remote bool // Whether the account is a local or remote one + Server string // The url of the server this account is from + DisplayName string // The display name of the user. Can be different from the handle + CustomFields []uint `gorm:"serializer:json"` // IDs to the custom fields a user has + Description string // The description of a user account + Tags []string `gorm:"serializer:json"` // Hashtags + IsBot bool // Whether to mark this account as a script controlled one + Follows []string `gorm:"serializer:json"` // List of handles this account follows + Followers []string `gorm:"serializer:json"` // List of handles that follow this account + Icon string // ID of a media file used as icon + Background string // ID of a media file used as background image + Banner string // ID of a media file used as banner + Indexable bool // Whether this account can be found by crawlers + PublicKeyPem string // The public key of the account + // Whether this account restricts following + // If true, the owner must approve of a follow request first + RestrictedFollow bool + // List of things the owner identifies as + // Example [cat human robot] means that the owner probably identifies as + // a cyborg-catgirl/boy/human + IdentifiesAs []Being + // List of pronouns the owner identifies with + // An unordered list since the owner can freely set it + // Examples: [she her], [it they its them] + Gender []string + + // --- And internal account stuff --- + // Still public fields since they wouldn't be able to be stored in the db otherwise + + PasswordHash []byte // Hash of the user's password + TotpToken []byte // Token for totp verification + // All the registered passkeys, name of passkey to credentials + // Could this be exported to another table? Yes + // Would it make sense? Probably not + // Will just take the performance hit of json conversion + // Access should be rare enough anyway + Passkeys map[string]webauthn.Credential `gorm:"serializer:json"` + PrivateKeyPem *string // The private key of the account. Nil if remote user +} + +func NewEmptyUser() *User { + return &User{ + ID: uuid.NewString(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Remote: false, + Server: "placeholder", + } +} diff --git a/storage/userIdentType.go b/storage/userIdentType.go new file mode 100644 index 0000000..8d94d61 --- /dev/null +++ b/storage/userIdentType.go @@ -0,0 +1,30 @@ +package storage + +import ( + "database/sql/driver" + "errors" +) + +type Being string + +const ( + BEING_HUMAN = Being("human") + BEING_CAT = Being("cat") + BEING_FOX = Being("fox") + BEING_DOG = Being("dog") + BEING_ROBOT = Being("robot") + BEING_DOLL = Being("doll") +) + +func (r *Being) Value() (driver.Value, error) { + return r, nil +} + +func (r *Being) Scan(raw any) error { + if v, ok := raw.(string); ok { + *r = Being(v) + return nil + } else { + return errors.New("value not a string") + } +} diff --git a/storage/userInfoFields.go b/storage/userInfoFields.go new file mode 100644 index 0000000..a3dc5ea --- /dev/null +++ b/storage/userInfoFields.go @@ -0,0 +1,14 @@ +package storage + +import ( + "time" + + "gorm.io/gorm" +) + +type UserInfoField struct { + gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3 + Name string + Value string + LastUrlCheckDate *time.Time // Used if the value is an url to somewhere. Empty if value is not an url +}