From 2977f09245b31a6731d6d44fb00000c073b3cf12 Mon Sep 17 00:00:00 2001 From: Melody Date: Wed, 28 Aug 2024 17:20:38 +0200 Subject: [PATCH] Sync --- .gitignore | 3 + 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/getRemoteUser.go | 27 +++ ap/parser.go | 59 ------ ap/type_parsers.go | 172 ---------------- ap/util.go | 19 ++ ap/webfinger.go | 87 ++++++++ config/config.go | 155 +++++++++++++- flags.go | 25 +++ frontend | 1 + go.mod | 41 ++-- go.sum | 82 +++++--- main.go | 51 +++++ server/endpoints_ap.go | 26 +-- server/middlewares/injectLogrus.go | 7 +- storage/errors.go | 7 + storage/mediaFile.go | 15 +- storage/notes.go | 22 +- storage/passkeySessions.go | 45 +++++ storage/remoteServerInfo.go | 14 +- storage/remoteUser.go | 5 + storage/roles.go | 8 + storage/storage.go | 32 +-- storage/user.go | 278 +++++++++++++++++++++----- storage/userIdentType.go | 18 -- 32 files changed, 763 insertions(+), 936 deletions(-) delete mode 100644 ap/common_types.go delete mode 100644 ap/constants.go delete mode 100644 ap/context.go delete mode 100644 ap/examples/masto_account.json delete mode 100644 ap/examples/masto_follow_request.json delete mode 100644 ap/examples/mk_note.json delete mode 100644 ap/examples/mk_note_create.json delete mode 100644 ap/explicit_types.go create mode 100644 ap/getRemoteUser.go delete mode 100644 ap/parser.go delete mode 100644 ap/type_parsers.go create mode 100644 ap/util.go create mode 100644 ap/webfinger.go create mode 100644 flags.go create mode 160000 frontend create mode 100644 main.go create mode 100644 storage/errors.go create mode 100644 storage/passkeySessions.go create mode 100644 storage/remoteUser.go create mode 100644 storage/roles.go diff --git a/.gitignore b/.gitignore index 2ba383b..c1300c8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ db.sqlite .env .lapce/ */tmp.* + +config.toml +.air.toml diff --git a/ap/common_types.go b/ap/common_types.go deleted file mode 100644 index 0d70934..0000000 --- a/ap/common_types.go +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 9e25637..0000000 --- a/ap/constants.go +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index 71f4a4f..0000000 --- a/ap/context.go +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 3f739b0..0000000 --- a/ap/examples/masto_account.json +++ /dev/null @@ -1,185 +0,0 @@ -[ - { - "@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 deleted file mode 100644 index d483b73..0000000 --- a/ap/examples/masto_follow_request.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "@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 deleted file mode 100644 index 7376e7d..0000000 --- a/ap/examples/mk_note.json +++ /dev/null @@ -1,41 +0,0 @@ -[ - { - "@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 deleted file mode 100644 index fce13ff..0000000 --- a/ap/examples/mk_note_create.json +++ /dev/null @@ -1,70 +0,0 @@ -[ - { - "@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 deleted file mode 100644 index 0617363..0000000 --- a/ap/explicit_types.go +++ /dev/null @@ -1,62 +0,0 @@ -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/getRemoteUser.go b/ap/getRemoteUser.go new file mode 100644 index 0000000..6876088 --- /dev/null +++ b/ap/getRemoteUser.go @@ -0,0 +1,27 @@ +package ap + +import ( + "net/http" + + "gitlab.com/mstarongitlab/goap" +) + +func GetRemoteUser(fullHandle string) (goap.BaseApChain, error) { + webfinger, err := GetAccountWebfinger(fullHandle) + if err != nil { + return nil, err + } + + apUrl := "" + for _, link := range webfinger.Links { + if link.Relation == "self" { + apUrl = *link.Href + } + } + apRequest, err := http.NewRequest("GET", apUrl, nil) + if err != nil { + return nil, err + } + apRequest.Header.Add("Accept", "application/activity+json,application/ld+json,application/json") + return nil, nil +} diff --git a/ap/parser.go b/ap/parser.go deleted file mode 100644 index f91d631..0000000 --- a/ap/parser.go +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 451f099..0000000 --- a/ap/type_parsers.go +++ /dev/null @@ -1,172 +0,0 @@ -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/ap/util.go b/ap/util.go new file mode 100644 index 0000000..b24a2a1 --- /dev/null +++ b/ap/util.go @@ -0,0 +1,19 @@ +package ap + +import "strings" + +type InvalidFullHandleError struct { + Raw string +} + +func (i InvalidFullHandleError) Error() string { + return "Invalid full handle" +} + +func SplitFullHandle(full string) (string, string, error) { + splits := strings.Split(strings.TrimPrefix(full, "@"), "@") + if len(splits) != 2 { + return "", "", InvalidFullHandleError{} + } + return splits[0], splits[1], nil +} diff --git a/ap/webfinger.go b/ap/webfinger.go new file mode 100644 index 0000000..4d15c02 --- /dev/null +++ b/ap/webfinger.go @@ -0,0 +1,87 @@ +package ap + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "time" +) + +// Data returned from a webfinger response (and also sent when asked for an account via webfinger) +type WebfingerData struct { + // What this webfinger data refers to. Accounts are usually `acct:username@host` + Subject string `json:"subject"` + // List of links related to the account + Links []struct { + // What type of link this is + // - `self` refers to the activitypub object and has Type and Href set + // - `http://webfinger.net/rel/profile-page` refers to the public webpage of the account and has Type and Href set + // - `http://ostatus.org/schema/1.0/subscribe` provides a template for subscribing/following the account. Has Template set + // Template will contain a `{uri}` part with which to replace idk yet + Relation string `json:"rel"` + // The content type of the url + Type *string `json:"type"` + // The url + Href *string `json:"href"` + // Template to use for something + Template *string `json:"template"` + } `json:"links"` +} + +var ErrAccountNotFound = errors.New("account not found") +var ErrRemoteServerFailed = errors.New("remote server errored out") + +// Get the webfinger data for a given full account handle (like @bob@example.com or bob@example.com) +func GetAccountWebfinger(fullHandle string) (*WebfingerData, error) { + // First get the handle and server domain from the full handle (and ensure it's a correctly formatted one) + handle, server, err := SplitFullHandle(fullHandle) + if err != nil { + return nil, err + } + webfingerRequest, err := http.NewRequest( + // Webfinger requests are GET + "GET", + // The webfinger url is located at /.well-known/webfinger + // The url parameter `resource` tells the remote server what data we want, should always be an account + // The prefix `acct:` (where full-handle is in the form of bob@example.com) + // tells the remote server that we want the account data of the given account handle + "https://"+server+"/.well-known/webfinger?resource=acct:"+handle+"@"+server, + // No request body since it's a get request and we only need the url parameter + nil, + ) + if err != nil { + return nil, err + } + // Make a http client with a timeout limit of 30 seconds + client := http.Client{Timeout: time.Second * 30} + // Then send the request + result, err := client.Do(webfingerRequest) + if err != nil { + return nil, err + } + + // Code 404 indicates that the account doesn't exist + if result.StatusCode == 404 { + return nil, ErrAccountNotFound + } else if result.StatusCode != 200 { + // And anything other than 200 or 404 is an error for now + // TODO: Add information gathering here to figure out what the remote server's problem is + return nil, ErrRemoteServerFailed + } + + // Get the body data from the response + data, err := io.ReadAll(result.Body) + if err != nil { + return nil, err + } + + // Then try and parse it into webfinger data + webfinger := WebfingerData{} + err = json.Unmarshal(data, &webfinger) + if err != nil { + return nil, err + } + + return &webfinger, nil +} diff --git a/config/config.go b/config/config.go index 4f13033..1b85427 100644 --- a/config/config.go +++ b/config/config.go @@ -1,26 +1,161 @@ package config +import ( + "fmt" + "os" + + "github.com/BurntSushi/toml" + "github.com/rs/zerolog/log" + "gitlab.com/mstarongitlab/goutils/other" +) + type ConfigSSL struct { - HandleSSL bool // Whether Linstrom should handle SSL encryption itself + HandleSSL bool `toml:"handle_ssl"` // Whether Linstrom should handle SSL encryption itself // If Linstrom is to handle SSL, whether it should use LetsEncrypt for certificates - UseLetsEncrypt *bool + UseLetsEncrypt *bool `toml:"use_lets_encrypt"` // Path to the certificate if Linstrom is to handle SSL while not using LetsEncrypt - CertificateFile *string + CertificateFile *string `toml:"certificate_file"` // Mail adress to use in case of using LetsEncrypt - AdminMail *string + AdminMail *string `toml:"admin_mail"` } type ConfigGeneral struct { - Domain string // The domain this server operates under + Protocol string `toml:"protocol"` // The protocol with which to access the server publicly (http/https) + // The subdomain under which the server lives (example: "linstrom" if the full domain is linstrom.example.com) + Subdomain *string `toml:"subdomain"` + // The root domain under which the server lives (example: "example.com" if the full domain is linstrom.example.com) + Domain string `toml:"domain"` + // The port on which the server runs on + PrivatePort int `toml:"private_port"` + // The port under which the public can reach the server (useful if running behind a reverse proxy) + PublicPort *int `toml:"public_port"` + // Url to the database to use. Can be a postgres url or the path to a sqlite file + Database string `toml:"database"` + // Whether the given database url is for a postgres server. If not set, assumes sqlite + DbIsPostgres *bool `toml:"db_is_postgres"` +} + +type ConfigWebAuthn struct { + DisplayName string `toml:"display_name"` + HashingSecret string `toml:"hashing_secret"` } type ConfigAdmin struct { - Username string - PasswordHash string + // Name of the server's root admin account + Username string `toml:"username"` + // A one time password used to verify account access to the root admin + // after a server has been created and before the account could be linked to a passkey + FirstTimeSetupOTP string `toml:"first_time_setup_otp"` } type Config struct { - General ConfigGeneral - SSL ConfigSSL - Admin ConfigAdmin + General ConfigGeneral `toml:"general"` + SSL ConfigSSL `toml:"ssl"` + Admin ConfigAdmin `toml:"admin"` + Webauthn ConfigWebAuthn `toml:"webauthn"` +} + +var GlobalConfig Config + +var defaultConfig Config = Config{ + General: ConfigGeneral{ + Protocol: "http", + Subdomain: nil, + Domain: "localhost", + PrivatePort: 8080, + PublicPort: nil, + Database: "db.sqlite", + DbIsPostgres: other.IntoPointer(false), + }, + SSL: ConfigSSL{ + HandleSSL: false, + UseLetsEncrypt: nil, + CertificateFile: nil, + AdminMail: nil, + }, + Admin: ConfigAdmin{ + Username: "server-admin", + FirstTimeSetupOTP: "Example otp password", + }, + Webauthn: ConfigWebAuthn{ + DisplayName: "Linstrom", + HashingSecret: "some super secure secret that should never be changed or else password storage breaks", + }, +} + +func (gc *ConfigGeneral) GetFullDomain() string { + if gc.Subdomain != nil { + return *gc.Subdomain + gc.Domain + } + return gc.Domain +} + +func (gc *ConfigGeneral) GetFullPublicUrl() string { + str := gc.Protocol + gc.GetFullDomain() + if gc.PublicPort != nil { + str += fmt.Sprint(*gc.PublicPort) + } else { + str += fmt.Sprint(gc.PrivatePort) + } + return str +} + +func WriteDefaultConfig(toFile string) error { + log.Trace().Caller().Send() + log.Info().Str("config-file", toFile).Msg("Writing default config to file") + file, err := os.Create(toFile) + if err != nil { + log.Error(). + Err(err). + Str("config-file", toFile). + Msg("Failed to create file for default config") + return err + } + defer file.Close() + + data, err := toml.Marshal(&defaultConfig) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal default config to toml") + return err + } + _, err = file.Write(data) + if err != nil { + log.Error().Err(err).Str("config-file", toFile).Msg("Failed to write default config") + return err + } + + log.Info().Str("config-file", toFile).Msg("Wrote default config") + return nil +} + +func ReadAndWriteToGlobal(fileName string) error { + log.Trace().Caller().Send() + log.Debug().Str("config-file", fileName).Msg("Attempting to read config file") + data, err := os.ReadFile(fileName) + if err != nil { + log.Warn(). + Str("config-file", fileName). + Err(err). + Msg("Failed to read config file, attempting to write default config") + err = WriteDefaultConfig(fileName) + if err != nil { + log.Error(). + Err(err). + Str("config-file", fileName). + Msg("Failed to create default config file") + return err + } + GlobalConfig = defaultConfig + return nil + } + config := Config{} + log.Debug().Str("config-file", fileName).Msg("Read config file, attempting to unmarshal") + err = toml.Unmarshal(data, &config) + if err != nil { + log.Error().Err(err).Bytes("config-data-raw", data).Msg("Failed to unmarshal config file") + return err + } + GlobalConfig = config + log.Info().Str("config-file", fileName).Msg("Read and applied config file") + return nil } diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..3875190 --- /dev/null +++ b/flags.go @@ -0,0 +1,25 @@ +package main + +import "flag" + +var ( + flagPrettyPrint *bool = flag.Bool( + "pretty", + false, + "If set, pretty prints logging entries which would be json objects otherwise", + ) + flagConfigFile *string = flag.String( + "config", + "config.toml", + "Location of the config file. Defaults to \"config.toml\"", + ) + flagLogLevel *string = flag.String( + "loglevel", + "Info", + "Set the logging level. Options are: Trace, Debug, Info, Warning, Error, Fatal. Capitalisation doesn't matter. Defaults to Info", + ) +) + +func init() { + flag.Parse() +} diff --git a/frontend b/frontend new file mode 160000 index 0000000..94f7c14 --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 94f7c146f67651d44c190163c8ac6312024dea90 diff --git a/go.mod b/go.mod index a168303..e8ccb6c 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,50 @@ module gitlab.com/mstarongitlab/linstrom -go 1.22.2 +go 1.23 + +toolchain go1.23.0 require ( - github.com/piprate/json-gold v0.5.0 - github.com/sirupsen/logrus v1.9.3 + github.com/BurntSushi/toml v1.4.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-webauthn/webauthn v0.11.2 + github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e + github.com/rs/zerolog v1.33.0 + gitlab.com/mstarongitlab/goutils v1.3.0 + gorm.io/driver/postgres v1.5.7 gorm.io/gorm v1.25.10 ) require ( + github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.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/go-webauthn/x v0.1.14 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/go-tpm v0.9.0 // indirect + github.com/google/go-tpm v0.9.1 // 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/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.9.0 // 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 + gitlab.com/mstarongitlab/goap v0.0.0-20240826151945-33a82e544bfb // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect diff --git a/go.sum b/go.sum index 7a158cb..71faad2 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,31 @@ +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38 h1:cUoduvNB9/JFJYEVeKy2hX/R5qyg2uwnHVhXCIjhBeI= +github.com/datainq/xml-date-time v0.0.0-20170820214645-2292f08baa38/go.mod h1:a0PnbhSGmzFMGx9KdEqfEdDHoEwTF9KLNbKLcWD8kAo= 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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.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/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= +github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= +github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= +github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= +github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 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= @@ -31,40 +38,55 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD 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/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e h1:BjuYFWZZd3O3RCMezWeO1CaJAOiSv2U3Pd8+nl9gDvA= +github.com/mstarongithub/passkey v0.0.0-20240817142622-de6912c8303e/go.mod h1:vsjtQX07PZmKGSwixqXoKg6bvo3GTCA0GIwjCQ6qpHI= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= +gitlab.com/mstarongitlab/goap v0.0.0-20240826151945-33a82e544bfb h1:3hzIIECbUNKm2ukwq31KjFc3+s2oixSxjW68HFcRGQM= +gitlab.com/mstarongitlab/goap v0.0.0-20240826151945-33a82e544bfb/go.mod h1:rt9IYvJBPh1z6t+vvzifmxDtGjGlr8683tSPfa5dbXI= +gitlab.com/mstarongitlab/goutils v1.3.0 h1:uuxPHjIU36lyJ8/z4T2xI32zOyh53Xj0Au8K12qkaJ4= +gitlab.com/mstarongitlab/goutils v1.3.0/go.mod h1:SvqfzFxgashuZPqR9kPwQ9gFA7I1yskZjhmGmY2pAow= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 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= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..70a3298 --- /dev/null +++ b/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "os" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "gitlab.com/mstarongitlab/linstrom/ap" + "gitlab.com/mstarongitlab/linstrom/config" +) + +func main() { + if *flagPrettyPrint { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + log.Info().Msg("Pretty logging enabled") + } + setLogLevel() + if err := config.ReadAndWriteToGlobal(*flagConfigFile); err != nil { + log.Fatal(). + Err(err). + Str("config-file", *flagConfigFile). + Msg("Failed to read config and couldn't write default") + } + // "@aufricus_athudath@activitypub.academy" + res, err := ap.GetAccountWebfinger("@aufricus_athudath@activitypub.academy") + log.Info(). + Err(err). + Any("webfinger", res). + Msg("Webfinger request result for @aufricus_athudath@activitypub.academy") +} + +func setLogLevel() { + log.Info().Str("new-level", *flagLogLevel).Msg("Attempting to set log level") + switch strings.ToLower(*flagLogLevel) { + case "trace": + zerolog.SetGlobalLevel(zerolog.TraceLevel) + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "fatal": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } +} diff --git a/server/endpoints_ap.go b/server/endpoints_ap.go index ce37bb1..e76176b 100644 --- a/server/endpoints_ap.go +++ b/server/endpoints_ap.go @@ -1,36 +1,38 @@ package server import ( + "errors" "fmt" "net/http" "strings" - "github.com/sirupsen/logrus" - "gitlab.com/mstarongitlab/linstrom/server/middlewares" + "github.com/rs/zerolog" "gitlab.com/mstarongitlab/linstrom/storage" + "gorm.io/gorm" ) // 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 - } + logger := zerolog.Ctx(r.Context()) 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") + logger.Debug().Msg("Resource parameter missing. Cancelling") return } accName := strings.TrimPrefix(requestedResource, "acc:") - acc, err := store.FindLocalAccount(accName) + acc, err := store.FindAccountByFullHandle(accName) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - logEntry.WithError(err).Warningln("couldn't find account") - return + if errors.Is(err, gorm.ErrRecordNotFound) { + http.Error(w, "account not found", http.StatusNotFound) + logger.Debug().Str("account-name", accName).Msg("Account not found") + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.Error().Err(err).Msg("Error while searching for account") + return + } } fmt.Fprint(w, acc) } diff --git a/server/middlewares/injectLogrus.go b/server/middlewares/injectLogrus.go index af68322..5566769 100644 --- a/server/middlewares/injectLogrus.go +++ b/server/middlewares/injectLogrus.go @@ -1,10 +1,9 @@ package middlewares import ( - "context" "net/http" - "github.com/sirupsen/logrus" + "github.com/rs/zerolog/log" ) const CONTEXT_KEY_LOGRUS = ContextKey("logrus") @@ -12,9 +11,7 @@ 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) + newContext := log.With().Str("url-path", r.URL.RawPath).Logger().WithContext(r.Context()) next.ServeHTTP(w, r.WithContext(newContext)) }) } diff --git a/storage/errors.go b/storage/errors.go new file mode 100644 index 0000000..a9bc1e1 --- /dev/null +++ b/storage/errors.go @@ -0,0 +1,7 @@ +package storage + +type NotImplementedError struct{} + +func (n NotImplementedError) Error() string { + return "Not implemented yet" +} diff --git a/storage/mediaFile.go b/storage/mediaFile.go index 6edd1fa..7ae3cae 100644 --- a/storage/mediaFile.go +++ b/storage/mediaFile.go @@ -7,9 +7,9 @@ import ( ) type MediaFile struct { - ID string `gorm:"primarykey"` // The unique ID of this media file - CreatedAt time.Time // When this entry was created - UpdatedAt time.Time // When this entry was last updated + ID string `gorm:"primarykey"` // The unique ID of this media file + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated // When this entry was deleted (for soft deletions) // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to // If not null, this entry is marked as deleted @@ -25,12 +25,3 @@ type MediaFile struct { // Caching user and server icons locally however should reduce burden on remote servers by quite a bit though LocallyCached bool } - -// Placeholder media file. Acts as placeholder for media file fields that have not been initialised yet but need a value -var placeholderMediaFile = &MediaFile{ - ID: "placeholder", - Remote: false, - Link: "placeholder", // TODO: Replace this with a file path to a staticly included image - Type: "image/png", - LocallyCached: true, -} diff --git a/storage/notes.go b/storage/notes.go index 930ec7f..f5f14d9 100644 --- a/storage/notes.go +++ b/storage/notes.go @@ -7,9 +7,9 @@ import ( ) type Note struct { - ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations - CreatedAt time.Time // When this entry was created - UpdatedAt time.Time // When this entry was last updated + ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated // When this entry was deleted (for soft deletions) // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to // If not null, this entry is marked as deleted @@ -29,19 +29,3 @@ type Note struct { OriginServer string // Url of the origin server. Also the primary key for those Tags []string `gorm:"serializer:json"` // Hashtags } - -var placeholderNote = &Note{ - ID: "placeholder", - Creator: "placeholder", - Remote: false, - RawContent: "placeholder", - ContentWarning: nil, - Attachments: []string{}, - Emotes: []string{}, - RepliesTo: nil, - Quotes: nil, - Target: NOTE_TARGET_HOME, - Pings: []string{}, - OriginServer: "placeholder", - Tags: []string{}, -} diff --git a/storage/passkeySessions.go b/storage/passkeySessions.go new file mode 100644 index 0000000..aaba918 --- /dev/null +++ b/storage/passkeySessions.go @@ -0,0 +1,45 @@ +package storage + +import ( + "github.com/go-webauthn/webauthn/webauthn" + "github.com/google/uuid" + "github.com/rs/zerolog/log" +) + +type PasskeySession struct { + ID string `gorm:"primarykey"` + Data webauthn.SessionData `gorm:"serializer:json"` +} + +// ---- Section SessionStore + +func (s *Storage) GenSessionID() (string, error) { + x := uuid.NewString() + log.Debug().Str("session-id", x).Msg("Generated new passkey session id") + return x, nil +} + +func (s *Storage) GetSession(sessionId string) (*webauthn.SessionData, bool) { + log.Debug().Str("id", sessionId).Msg("Looking for passkey session") + session := PasskeySession{} + res := s.db.Where("id = ?", sessionId).First(&session) + if res.Error != nil { + return nil, false + } + log.Debug().Str("id", sessionId).Any("webauthn-data", &session).Msg("Found passkey session") + return &session.Data, true +} + +func (s *Storage) SaveSession(token string, data *webauthn.SessionData) { + log.Debug().Str("id", token).Any("webauthn-data", data).Msg("Saving passkey session") + session := PasskeySession{ + ID: token, + Data: *data, + } + s.db.Save(&session) +} + +func (s *Storage) DeleteSession(token string) { + log.Debug().Str("id", token).Msg("Deleting passkey session (if one exists)") + s.db.Delete(&PasskeySession{ID: token}) +} diff --git a/storage/remoteServerInfo.go b/storage/remoteServerInfo.go index 395b97a..860cda7 100644 --- a/storage/remoteServerInfo.go +++ b/storage/remoteServerInfo.go @@ -7,9 +7,9 @@ import ( ) type RemoteServer struct { - ID string `gorm:"primarykey"` // ID is also server url - CreatedAt time.Time // When this entry was created - UpdatedAt time.Time // When this entry was last updated + ID string `gorm:"primarykey"` // ID is also server url + CreatedAt time.Time // When this entry was created + UpdatedAt time.Time // When this entry was last updated // When this entry was deleted (for soft deletions) // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to // If not null, this entry is marked as deleted @@ -19,11 +19,3 @@ type RemoteServer struct { Icon string // ID of a media file IsSelf bool // Whether this server is yours truly } - -var placeholderServer = &RemoteServer{ - ID: "placeholder", - ServerType: REMOTE_SERVER_LINSTROM, - Name: "placeholder", - Icon: "placeholder", - IsSelf: false, -} diff --git a/storage/remoteUser.go b/storage/remoteUser.go new file mode 100644 index 0000000..2157ae9 --- /dev/null +++ b/storage/remoteUser.go @@ -0,0 +1,5 @@ +package storage + +func (s *Storage) NewRemoteUser(fullHandle string) (*Account, error) { + return nil, nil +} diff --git a/storage/roles.go b/storage/roles.go new file mode 100644 index 0000000..55d8c31 --- /dev/null +++ b/storage/roles.go @@ -0,0 +1,8 @@ +package storage + +type Role struct { + // Name of the role + Name string + // If set, counts as all permissions being set and all restrictions being disabled + FullAdmin bool +} diff --git a/storage/storage.go b/storage/storage.go index 12c92fd..1d2d8c8 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1,8 +1,6 @@ package storage import ( - "fmt" - "github.com/glebarez/sqlite" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -35,34 +33,16 @@ func storageFromEmptyDb(db *gorm.DB) (*Storage, error) { // AutoMigrate ensures the db is in a state where all the structs given here // have their own tables and relations setup. It also updates tables if necessary db.AutoMigrate( - placeholderMediaFile, - placeholderUser(), - placeholderNote, - placeholderServer, + MediaFile{}, + Account{}, + RemoteServer{}, + Note{}, + Role{}, + PasskeySession{}, ) - // Afterwards add the placeholder entries for each table. - // FirstOrCreate either creates a new entry or retrieves the first matching one - // We only care about the creation if there is none yet, so no need to carry the result over - if res := db.FirstOrCreate(placeholderMediaFile); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) - } - if res := db.FirstOrCreate(placeholderUser()); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) - } - if res := db.FirstOrCreate(placeholderNote); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) - } - if res := db.FirstOrCreate(placeholderServer); res.Error != nil { - return nil, fmt.Errorf("failed to add placeholder media file: %w", res.Error) - } // And finally, build the actual storage struct 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 index fed4001..47a3590 100644 --- a/storage/user.go +++ b/storage/user.go @@ -1,21 +1,30 @@ package storage import ( + "crypto/rand" + "errors" "time" "github.com/go-webauthn/webauthn/webauthn" - "github.com/google/uuid" + "github.com/mstarongithub/passkey" + "github.com/rs/zerolog/log" + "gitlab.com/mstarongitlab/linstrom/ap" + "gitlab.com/mstarongitlab/linstrom/config" "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 a uuid for this account - Handle string // Handle 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 +type Account struct { + ID string `gorm:"primarykey"` // ID is a uuid for this account + // Handle of the user (eg "max" if the full username is @max@example.com) + // Assume unchangable (once set by a user) to be kind to other implementations + // Would be an easy avenue to fuck with them though + Handle string + CreatedAt time.Time // When this entry was created. Automatically set by gorm + // When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm + UpdatedAt time.Time // When this entry was deleted (for soft deletions) // Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to // If not null, this entry is marked as deleted @@ -39,57 +48,230 @@ type User struct { 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 + // a cyborg-catgirl/boy/human or a cathuman shaped robot, refer to Gender for pronouns + IdentifiesAs []Being `gorm:"serializer:json"` // 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 + Gender []string `gorm:"serializer:json"` + // The roles assocciated with an account + Roles []string `gorm:"serializer:json"` // --- 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 + PrivateKeyPem *string // The private key of the account. Nil if remote user + WebAuthnId []byte // The unique and random ID of this account used for passkey authentication + // Whether the account got verified and is allowed to be active + // For local accounts being active means being allowed to login and perform interactions + // For remote users, if an account is not verified, any interactions it sends are discarded + Verified bool + PasskeyCredentials []webauthn.Credential `gorm:"serializer:json"` // Webauthn credentials data + // Has a RemoteAccountLinks included if remote user + RemoteLinks *RemoteAccountLinks } -func NewEmptyUser() *User { - return &User{ - ID: uuid.NewString(), - Handle: "placeholder", - Remote: false, - Server: "placeholder", - DisplayName: "placeholder", - CustomFields: []uint{}, - Description: "placeholder", - Tags: []string{}, - IsBot: true, - Follows: []string{}, - Followers: []string{}, - Icon: "placeholder", - Background: "placeholder", - Banner: "placeholder", - Indexable: false, - PublicKeyPem: nil, - RestrictedFollow: false, - IdentifiesAs: []Being{BEING_ROBOT}, - Gender: []string{"it", "its"}, - PasswordHash: []byte("placeholder"), - TotpToken: []byte("placeholder"), - Passkeys: map[string]webauthn.Credential{}, - PrivateKeyPem: nil, +// Contains static and cached info about a remote account, mostly links +type RemoteAccountLinks struct { + // ---- Section: gorm + // Sets this struct up as a value that an Account may have + gorm.Model + AccountID string + + // Just about every link here is optional to accomodate for servers with only minimal accounts + // Minimal being handle, ap link and inbox + ApLink string + ViewLink *string + FollowersLink *string + FollowingLink *string + InboxLink string + OutboxLink *string + FeaturedLink *string + FeaturedTagsLink *string +} + +// Find an account in the db using a given full handle (@max@example.com) +// Returns an account and nil if an account is found, otherwise nil and the error +func (s *Storage) FindAccountByFullHandle(handle string) (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Str("account-handle", handle).Msg("Looking for account by handle") + name, server, err := ap.SplitFullHandle(handle) + if err != nil { + log.Warn().Err(err).Str("account-handle", handle).Msg("Failed to split up account handle") + return nil, err } + + acc := Account{} + res := s.db.Where("name = ?", name).Where("server = ?", server).First(&acc) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + log.Info().Str("account-handle", handle).Msg("Account with handle not found") + } else { + log.Error().Err(err).Str("account-handle", handle).Msg("Failed to get account with handle") + } + return nil, res.Error + } + log.Info().Str("account-handle", handle).Msg("Found account") + return &acc, nil } -func placeholderUser() *User { - tmp := NewEmptyUser() - tmp.ID = "placeholder" - return tmp +// Find an account given a specific ID +func (s *Storage) FindAccountById(id string) (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Str("account-id", id).Msg("Looking for account by id") + acc := Account{} + res := s.db.First(&acc, id) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + log.Warn().Str("account-id", id).Msg("Account not found") + } else { + log.Error().Err(res.Error).Str("account-id", id).Msg("Failed to look for account") + } + return nil, res.Error + } + log.Info().Str("account-id", id).Msg("Found account") + return &acc, nil +} + +func (s *Storage) NewEmptyAccount() (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Msg("Creating new empty account") + acc := Account{} + // Generate the 64 bit id for passkey and webauthn stuff + data := make([]byte, 64) + c, err := rand.Read(data) + for err != nil || c != len(data) || c < 64 { + data = make([]byte, 64) + c, err = rand.Read(data) + } + acc.WebAuthnId = data + acc.Followers = []string{} + acc.Tags = []string{} + acc.Follows = []string{} + acc.Gender = []string{} + acc.CustomFields = []uint{} + acc.IdentifiesAs = []Being{} + acc.PasskeyCredentials = []webauthn.Credential{} + res := s.db.Save(acc) + if res.Error != nil { + log.Error().Err(res.Error).Msg("Failed to safe new account") + return nil, res.Error + } + log.Info().Str("account-id", acc.ID).Msg("Created new account") + return &acc, nil +} + +func (s *Storage) NewLocalAccount(handle string) (*Account, error) { + log.Trace().Caller().Send() + log.Debug().Str("account-handle", handle).Msg("Creating new local account") + acc, err := s.NewEmptyAccount() + if err != nil { + log.Error().Err(err).Msg("Failed to create empty account for use") + return nil, err + } + acc.Handle = handle + acc.Server = config.GlobalConfig.General.GetFullDomain() + acc.Remote = false + acc.DisplayName = handle + + log.Debug(). + Str("account-handle", handle). + Str("account-id", acc.ID). + Msg("Saving new local account") + res := s.db.Save(acc) + if res.Error != nil { + log.Error().Err(res.Error).Any("account-full", acc).Msg("Failed to save local account") + return nil, res.Error + } + log.Info(). + Str("account-handle", handle). + Str("account-id", acc.ID). + Msg("Created new local account") + return acc, nil +} + +// ---- Section WebAuthn.User +// Implements the webauthn.User interface for interaction with passkeys + +func (a *Account) WebAuthnID() []byte { + log.Trace().Caller().Send() + return a.WebAuthnId +} + +func (u *Account) WebAuthnName() string { + log.Trace().Caller().Send() + return u.Handle +} + +func (u *Account) WebAuthnDisplayName() string { + log.Trace().Caller().Send() + return u.DisplayName +} + +func (u *Account) WebAuthnCredentials() []webauthn.Credential { + log.Trace().Caller().Send() + return u.PasskeyCredentials +} + +func (u *Account) WebAuthnIcon() string { + log.Trace().Caller().Send() + return "" +} + +// ---- Section passkey.User +// Implements the passkey.User interface + +func (u *Account) PutCredential(new webauthn.Credential) { + log.Trace().Caller().Send() + u.PasskeyCredentials = append(u.PasskeyCredentials, new) +} + +// Section passkey.UserStore +// Implements the passkey.UserStore interface + +func (s *Storage) GetOrCreateUser(userID string) passkey.User { + log.Trace().Caller().Send() + log.Debug(). + Str("account-handle", userID). + Msg("Looking for or creating account for passkey stuff") + acc := &Account{} + res := s.db.Where(Account{Handle: userID, Server: config.GlobalConfig.General.GetFullDomain()}). + First(acc) + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + log.Debug().Str("account-handle", userID) + var err error + acc, err = s.NewLocalAccount(userID) + if err != nil { + log.Error(). + Err(err). + Str("account-handle", userID). + Msg("Failed to create new account for webauthn request") + return nil + } + } + return acc +} + +func (s *Storage) GetUserByWebAuthnId(id []byte) passkey.User { + log.Trace().Caller().Send() + log.Debug().Bytes("webauthn-id", id).Msg("Looking for account with webauthn id") + acc := Account{} + res := s.db.Where(Account{WebAuthnId: id}).First(&acc) + if res.Error != nil { + log.Error(). + Err(res.Error). + Bytes("webauthn-id", id). + Msg("Failed to find user with webauthn ID") + return nil + } + log.Info().Msg("Found account with given webauthn id") + return &acc +} + +func (s *Storage) SaveUser(rawUser passkey.User) { + log.Trace().Caller().Send() + user, ok := rawUser.(*Account) + if !ok { + log.Error().Any("raw-user", rawUser).Msg("Failed to cast raw user to db account") + } + s.db.Save(user) } diff --git a/storage/userIdentType.go b/storage/userIdentType.go index 7ce7df5..3693853 100644 --- a/storage/userIdentType.go +++ b/storage/userIdentType.go @@ -1,10 +1,5 @@ package storage -import ( - "database/sql/driver" - "errors" -) - // What kind of being a user identifies as type Being string @@ -16,16 +11,3 @@ const ( 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") - } -}