diff --git a/internal/api/s2s/user/userget.go b/internal/api/s2s/user/userget.go index 4828cbbd3..8df137f44 100644 --- a/internal/api/s2s/user/userget.go +++ b/internal/api/s2s/user/userget.go @@ -56,7 +56,7 @@ func (m *Module) UsersGETHandler(c *gin.Context) { // make a copy of the context to pass along so we don't break anything cp := c.Copy() - user, err := m.processor.GetAPUser(requestedUsername, cp.Request) // GetAPUser handles auth as well + user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetAPUser handles auth as well if err != nil { l.Info(err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) diff --git a/internal/federation/util.go b/internal/federation/util.go index 0467892f8..4adce52b2 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -234,50 +234,3 @@ func (f *federator) GetTransportForUser(username string) (pub.Transport, error) } return transport, nil } - -const ( - activityStreamsContext = "https://www.w3.org/ns/activitystreams" - w3idContext = "https://w3id.org/security/v1" - tootContext = "http://joinmastodon.org/ns#" - schemaContext = "http://schema.org#" -) - -// ActivityStreamsContext returns the url representation of https://www.w3.org/ns/activitystreams -func ActivityStreamsContext() *url.URL { - u, err := url.Parse(activityStreamsContext) - if err != nil { - panic(err) - } - return u -} - -// W3IDContext returns the url representation of https://w3id.org/security/v1 -func W3IDContext() *url.URL { - u, err := url.Parse(w3idContext) - if err != nil { - panic(err) - } - return u -} - -// TootContext returns the url representation of http://joinmastodon.org/ns# -func TootContext() *url.URL { - u, err := url.Parse(tootContext) - if err != nil { - panic(err) - } - return u -} - -// SchemaContext returns the url representation of http://schema.org# -func SchemaContext() *url.URL { - u, err := url.Parse(schemaContext) - if err != nil { - panic(err) - } - return u -} - -func StandardContexts() vocab.ActivityStreamsContextProperty { - return nil -} diff --git a/internal/message/apuserprocess.go b/internal/message/apuserprocess.go deleted file mode 100644 index dc4b861e6..000000000 --- a/internal/message/apuserprocess.go +++ /dev/null @@ -1,70 +0,0 @@ -package message - -import ( - "fmt" - "net/http" - - "github.com/go-fed/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) GetAPUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) { - // get the account the request is referring to - requestedAccount := >smodel.Account{} - if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) - if err != nil { - return nil, NewErrorNotAuthorized(err) - } - - requestingAccount := >smodel.Account{} - err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount) - if err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - // we don't have an entry for this account yet - // what we do now should depend on our chosen federation method - // for now though, we'll just dereference it - // TODO: slow-fed - requestingPerson, err := p.federator.DereferenceRemoteAccount(requestedUsername, requestingAccountURI) - if err != nil { - return nil, NewErrorInternalError(err) - } - requestedAccount, err = p.tc.ASPersonToAccount(requestingPerson) - if err != nil { - return nil, NewErrorInternalError(err) - } - if err := p.db.Put(requestingAccount); err != nil { - return nil, NewErrorInternalError(err) - } - } else { - // something has actually gone wrong - return nil, NewErrorInternalError(err) - } - } - - blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) - if err != nil { - return nil, NewErrorInternalError(err) - } - - if blocked { - return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - requestedPerson, err := p.tc.AccountToAS(requestedAccount) - if err != nil { - return nil, NewErrorInternalError(err) - } - - data, err := streams.Serialize(requestedPerson) - if err != nil { - return nil, NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go new file mode 100644 index 000000000..2402c89bb --- /dev/null +++ b/internal/message/fediprocess.go @@ -0,0 +1,102 @@ +package message + +import ( + "fmt" + "net/http" + + "github.com/go-fed/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// authenticateAndDereferenceFediRequest authenticates the HTTP signature of an incoming federation request, using the given +// username to perform the validation. It will *also* dereference the originator of the request and return it as a gtsmodel account +// for further processing. NOTE that this function will have the side effect of putting the dereferenced account into the database, +// and passing it into the processor through a channel for further asynchronous processing. +func (p *processor) authenticateAndDereferenceFediRequest(username string, r *http.Request) (*gtsmodel.Account, error) { + + // first authenticate + requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(username, r) + if err != nil { + return nil, fmt.Errorf("couldn't authenticate request for username %s: %s", username, err) + } + + // OK now we can do the dereferencing part + // we might already have an entry for this account so check that first + requestingAccount := >smodel.Account{} + + err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount) + if err == nil { + // we do have it yay, return it + return requestingAccount, nil + } + + if _, ok := err.(db.ErrNoEntries); !ok { + // something has actually gone wrong so bail + return nil, fmt.Errorf("database error getting account with uri %s: %s", requestingAccountURI.String(), err) + } + + // we just don't have an entry for this account yet + // what we do now should depend on our chosen federation method + // for now though, we'll just dereference it + // TODO: slow-fed + requestingPerson, err := p.federator.DereferenceRemoteAccount(username, requestingAccountURI) + if err != nil { + return nil, fmt.Errorf("couldn't dereference %s: %s", requestingAccountURI.String(), err) + } + + // convert it to our internal account representation + requestingAccount, err = p.tc.ASPersonToAccount(requestingPerson) + if err != nil { + return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err) + } + + // shove it in the database for later + if err := p.db.Put(requestingAccount); err != nil { + return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err) + } + + // put it in our channel to queue it for async processing + p.FromFederator() <- FromFederator{ + APObjectType: gtsmodel.ActivityStreamsProfile, + APActivityType: gtsmodel.ActivityStreamsCreate, + Activity: requestingAccount, + } + + return requestingAccount, nil +} + +func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) { + // get the account the request is referring to + requestedAccount := >smodel.Account{} + if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request) + if err != nil { + return nil, NewErrorNotAuthorized(err) + } + + blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) + if err != nil { + return nil, NewErrorInternalError(err) + } + + if blocked { + return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + requestedPerson, err := p.tc.AccountToAS(requestedAccount) + if err != nil { + return nil, NewErrorInternalError(err) + } + + data, err := streams.Serialize(requestedPerson) + if err != nil { + return nil, NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/message/processor.go b/internal/message/processor.go index d8620f662..e748cd15c 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -46,8 +46,12 @@ type Processor interface { FromClientAPI() chan FromClientAPI // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub). ToFederator() chan ToFederator - // FromFederator returns a channel for putting messages in that come from the federator going into the processor + // FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor FromFederator() chan FromFederator + // Start starts the Processor, reading from its channels and passing messages back and forth. + Start() error + // Stop stops the processor cleanly, finishing handling any remaining messages before closing down. + Stop() error /* CLIENT API-FACING PROCESSING FUNCTIONS @@ -80,6 +84,7 @@ type Processor interface { // MediaCreate handles the creation of a media attachment, using the given form. MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) + // MediaGet handles the fetching of a media attachment, using the given request form. MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) @@ -92,12 +97,12 @@ type Processor interface { response, pass work to the processor using a channel instead. */ - GetAPUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) + // GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication + // before returning a JSON serializable interface to the caller. + GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) + + - // Start starts the Processor, reading from its channels and passing messages back and forth. - Start() error - // Stop stops the processor cleanly, finishing handling any remaining messages before closing down. - Stop() error } // processor just implements the Processor interface @@ -161,8 +166,12 @@ func (p *processor) Start() error { select { case clientMsg := <-p.toClientAPI: p.log.Infof("received message TO client API: %+v", clientMsg) + case clientMsg := <-p.fromClientAPI: + p.log.Infof("received message FROM client API: %+v", clientMsg) case federatorMsg := <-p.toFederator: p.log.Infof("received message TO federator: %+v", federatorMsg) + case federatorMsg := <-p.fromFederator: + p.log.Infof("received message FROM federator: %+v", federatorMsg) case <-p.stop: break DistLoop } diff --git a/internal/typeutils/asextractionutil.go b/internal/typeutils/asextractionutil.go new file mode 100644 index 000000000..27d3bc9d1 --- /dev/null +++ b/internal/typeutils/asextractionutil.go @@ -0,0 +1,183 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package typeutils + +import ( + "errors" + "net/url" + + "github.com/go-fed/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/media" +) + +type usernameable interface { + GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty +} + +type iconable interface { + GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty +} + +type displaynameable interface { + GetActivityStreamsName() vocab.ActivityStreamsNameProperty +} + +type imageable interface { + GetActivityStreamsImage() vocab.ActivityStreamsImageProperty +} + +func extractPreferredUsername(i usernameable) (string, error) { + u := i.GetActivityStreamsPreferredUsername() + if u == nil || !u.IsXMLSchemaString() { + return "", errors.New("preferredUsername was not a string") + } + if u.GetXMLSchemaString() == "" { + return "", errors.New("preferredUsername was empty") + } + return u.GetXMLSchemaString(), nil +} + +func extractName(i displaynameable) (string, error) { + nameProp := i.GetActivityStreamsName() + if nameProp == nil { + return "", errors.New("activityStreamsName not found") + } + + // take the first name string we can find + for nameIter := nameProp.Begin(); nameIter != nameProp.End(); nameIter = nameIter.Next() { + if nameIter.IsXMLSchemaString() && nameIter.GetXMLSchemaString() != "" { + return nameIter.GetXMLSchemaString(), nil + } + } + + return "", errors.New("activityStreamsName not found") +} + +// extractIconURL extracts a URL to a supported image file from something like: +// "icon": { +// "mediaType": "image/jpeg", +// "type": "Image", +// "url": "http://example.org/path/to/some/file.jpeg" +// }, +func extractIconURL(i iconable) (*url.URL, error) { + iconProp := i.GetActivityStreamsIcon() + if iconProp == nil { + return nil, errors.New("icon property was nil") + } + + // icon can potentially contain multiple entries, so we iterate through all of them + // here in order to find the first one that meets these criteria: + // 1. is an image + // 2. is a supported type + // 3. has a URL so we can grab it + for iconIter := iconProp.Begin(); iconIter != iconProp.End(); iconIter = iconIter.Next() { + // 1. is an image + if !iconIter.IsActivityStreamsImage() { + continue + } + imageValue := iconIter.GetActivityStreamsImage() + if imageValue == nil { + continue + } + + // 2. is a supported type + imageType := imageValue.GetActivityStreamsMediaType() + if imageType == nil || !media.SupportedImageType(imageType.Get()) { + continue + } + + // 3. has a URL so we can grab it + imageURLProp := imageValue.GetActivityStreamsUrl() + if imageURLProp == nil { + continue + } + + // URL is also an iterable! + // so let's take the first valid one we can find + for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() { + if !urlIter.IsIRI() { + continue + } + if urlIter.GetIRI() == nil { + continue + } + // found it!!! + return urlIter.GetIRI(), nil + } + } + // if we get to this point we didn't find an icon meeting our criteria :'( + return nil, errors.New("could not extract valid image from icon") +} + + +// extractImageURL extracts a URL to a supported image file from something like: +// "image": { +// "mediaType": "image/jpeg", +// "type": "Image", +// "url": "http://example.org/path/to/some/file.jpeg" +// }, +func extractImageURL(i imageable) (*url.URL, error) { + imageProp := i.GetActivityStreamsImage() + if imageProp == nil { + return nil, errors.New("icon property was nil") + } + + // icon can potentially contain multiple entries, so we iterate through all of them + // here in order to find the first one that meets these criteria: + // 1. is an image + // 2. is a supported type + // 3. has a URL so we can grab it + for imageIter := imageProp.Begin(); imageIter != imageProp.End(); imageIter = imageIter.Next() { + // 1. is an image + if !imageIter.IsActivityStreamsImage() { + continue + } + imageValue := imageIter.GetActivityStreamsImage() + if imageValue == nil { + continue + } + + // 2. is a supported type + imageType := imageValue.GetActivityStreamsMediaType() + if imageType == nil || !media.SupportedImageType(imageType.Get()) { + continue + } + + // 3. has a URL so we can grab it + imageURLProp := imageValue.GetActivityStreamsUrl() + if imageURLProp == nil { + continue + } + + // URL is also an iterable! + // so let's take the first valid one we can find + for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() { + if !urlIter.IsIRI() { + continue + } + if urlIter.GetIRI() == nil { + continue + } + // found it!!! + return urlIter.GetIRI(), nil + } + } + // if we get to this point we didn't find an image meeting our criteria :'( + return nil, errors.New("could not extract valid image from image property") +} diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 73809c57b..fbecb8af7 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -1,14 +1,30 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + package typeutils import ( "errors" "fmt" - "net/url" "github.com/go-fed/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/media" ) func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) { @@ -34,16 +50,15 @@ func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsm acct = >smodel.Account{} acct.URI = uri.String() - // Username + // Username aka preferredUsername // We need this one so bail if it's not set. - username, err := extractUsername(person) + username, err := extractPreferredUsername(person) if err != nil { return nil, fmt.Errorf("couldn't extract username: %s", err) } acct.Username = username // Domain - // We need this one as well acct.Domain = uri.Host // avatar aka icon @@ -58,140 +73,54 @@ func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsm acct.HeaderRemoteURL = headerURL.String() } + // display name aka name + // we default to the username, but take the more nuanced name property if it exists + acct.DisplayName = username + if displayName, err := extractName(person); err == nil { + acct.DisplayName = displayName + } + + // fields aka attachment array + // TODO + + // note aka summary + // TODO + + // bot + // TODO: parse this from application vs. person type + + // locked aka manuallyApprovesFollowers + // TODO + + // discoverable + // TODO + + // url property + // TODO + + // InboxURI + // TODO + + // OutboxURI + // TODO + + // FollowingURI + // TODO + + // FollowersURI + // TODO + + // FeaturedURI + // TODO + + // FeaturedTagsURI + // TODO + + // alsoKnownAs + // TODO + + // publicKey + // TODO + return acct, nil } - -type usernameable interface { - GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty -} - -func extractUsername(i usernameable) (string, error) { - u := i.GetActivityStreamsPreferredUsername() - if u == nil || !u.IsXMLSchemaString() { - return "", errors.New("preferredUsername was not a string") - } - if u.GetXMLSchemaString() == "" { - return "", errors.New("preferredUsername was empty") - } - return u.GetXMLSchemaString(), nil -} - -type iconable interface { - GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty -} - -// extractIconURL extracts a URL to a supported image file from something like: -// "icon": { -// "mediaType": "image/jpeg", -// "type": "Image", -// "url": "http://example.org/path/to/some/file.jpeg" -// }, -func extractIconURL(i iconable) (*url.URL, error) { - iconProp := i.GetActivityStreamsIcon() - if iconProp == nil { - return nil, errors.New("icon property was nil") - } - - // icon can potentially contain multiple entries, so we iterate through all of them - // here in order to find the first one that meets these criteria: - // 1. is an image - // 2. is a supported type - // 3. has a URL so we can grab it - for iconIter := iconProp.Begin(); iconIter != iconProp.End(); iconIter = iconIter.Next() { - // 1. is an image - if !iconIter.IsActivityStreamsImage() { - continue - } - imageValue := iconIter.GetActivityStreamsImage() - if imageValue == nil { - continue - } - - // 2. is a supported type - imageType := imageValue.GetActivityStreamsMediaType() - if imageType == nil || !media.SupportedImageType(imageType.Get()) { - continue - } - - // 3. has a URL so we can grab it - imageURLProp := imageValue.GetActivityStreamsUrl() - if imageURLProp == nil { - continue - } - - // URL is also an iterable! - // so let's take the first valid one we can find - for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() { - if !urlIter.IsIRI() { - continue - } - if urlIter.GetIRI() == nil { - continue - } - // found it!!! - return urlIter.GetIRI(), nil - } - } - // if we get to this point we didn't find an icon meeting our criteria :'( - return nil, errors.New("could not extract valid image from icon") -} - -type imageable interface { - GetActivityStreamsImage() vocab.ActivityStreamsImageProperty -} - -// extractImageURL extracts a URL to a supported image file from something like: -// "image": { -// "mediaType": "image/jpeg", -// "type": "Image", -// "url": "http://example.org/path/to/some/file.jpeg" -// }, -func extractImageURL(i imageable) (*url.URL, error) { - imageProp := i.GetActivityStreamsImage() - if imageProp == nil { - return nil, errors.New("icon property was nil") - } - - // icon can potentially contain multiple entries, so we iterate through all of them - // here in order to find the first one that meets these criteria: - // 1. is an image - // 2. is a supported type - // 3. has a URL so we can grab it - for imageIter := imageProp.Begin(); imageIter != imageProp.End(); imageIter = imageIter.Next() { - // 1. is an image - if !imageIter.IsActivityStreamsImage() { - continue - } - imageValue := imageIter.GetActivityStreamsImage() - if imageValue == nil { - continue - } - - // 2. is a supported type - imageType := imageValue.GetActivityStreamsMediaType() - if imageType == nil || !media.SupportedImageType(imageType.Get()) { - continue - } - - // 3. has a URL so we can grab it - imageURLProp := imageValue.GetActivityStreamsUrl() - if imageURLProp == nil { - continue - } - - // URL is also an iterable! - // so let's take the first valid one we can find - for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() { - if !urlIter.IsIRI() { - continue - } - if urlIter.GetIRI() == nil { - continue - } - // found it!!! - return urlIter.GetIRI(), nil - } - } - // if we get to this point we didn't find an image meeting our criteria :'( - return nil, errors.New("could not extract valid image from image property") -} diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go new file mode 100644 index 000000000..3f1ba4799 --- /dev/null +++ b/internal/typeutils/astointernal_test.go @@ -0,0 +1,66 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package typeutils_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type ASToInternalTestSuite struct { + ConverterStandardTestSuite +} + +func (suite *ASToInternalTestSuite) SetupSuite() { + suite.config = testrig.NewTestConfig() + suite.db = testrig.NewTestDB() + suite.log = testrig.NewTestLog() + suite.accounts = testrig.NewTestAccounts() + suite.people = testrig.NewTestFediPeople() + suite.typeconverter = typeutils.NewConverter(suite.config, suite.db) +} + +func (suite *ASToInternalTestSuite) SetupTest() { + testrig.StandardDBSetup(suite.db) +} + +func (suite *ASToInternalTestSuite) TestASPersonToAccount() { + + testPerson := suite.people["new_person_1"] + + acct, err := suite.typeconverter.ASPersonToAccount(testPerson) + assert.NoError(suite.T(), err) + + fmt.Printf("%+v", acct) + // TODO: write assertions here, rn we're just eyeballing the output + +} + +func (suite *ASToInternalTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) +} + +func TestASToInternalTestSuite(t *testing.T) { + suite.Run(t, new(ASToInternalTestSuite)) +} diff --git a/internal/typeutils/converter_test.go b/internal/typeutils/converter_test.go new file mode 100644 index 000000000..f94541c72 --- /dev/null +++ b/internal/typeutils/converter_test.go @@ -0,0 +1,40 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package typeutils_test + +import ( + "github.com/go-fed/activity/streams/vocab" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" +) + +type ConverterStandardTestSuite struct { + suite.Suite + config *config.Config + db db.DB + log *logrus.Logger + accounts map[string]*gtsmodel.Account + people map[string]vocab.ActivityStreamsPerson + + typeconverter typeutils.TypeConverter +} diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 1d1242a6e..22cac2a4d 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -230,9 +230,9 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso // image // Used as profile header. if a.HeaderMediaAttachmentID != "" { - iconProperty := streams.NewActivityStreamsIconProperty() + headerProperty := streams.NewActivityStreamsImageProperty() - iconImage := streams.NewActivityStreamsImage() + headerImage := streams.NewActivityStreamsImage() header := >smodel.MediaAttachment{} if err := c.db.GetByID(a.HeaderMediaAttachmentID, header); err != nil { @@ -241,7 +241,7 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso mediaType := streams.NewActivityStreamsMediaTypeProperty() mediaType.Set(header.File.ContentType) - iconImage.SetActivityStreamsMediaType(mediaType) + headerImage.SetActivityStreamsMediaType(mediaType) headerURLProperty := streams.NewActivityStreamsUrlProperty() headerURL, err := url.Parse(header.URL) @@ -249,9 +249,9 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso return nil, err } headerURLProperty.AppendIRI(headerURL) - iconImage.SetActivityStreamsUrl(headerURLProperty) + headerImage.SetActivityStreamsUrl(headerURLProperty) - iconProperty.AppendActivityStreamsImage(iconImage) + headerProperty.AppendActivityStreamsImage(headerImage) } return person, nil diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index 4e9236e88..8ccfbc48c 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -24,25 +24,15 @@ import ( "testing" "github.com/go-fed/activity/streams" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/testrig" ) type InternalToASTestSuite struct { - suite.Suite - config *config.Config - db db.DB - log *logrus.Logger - accounts map[string]*gtsmodel.Account - - typeconverter typeutils.TypeConverter + ConverterStandardTestSuite } // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout @@ -52,6 +42,7 @@ func (suite *InternalToASTestSuite) SetupSuite() { suite.db = testrig.NewTestDB() suite.log = testrig.NewTestLog() suite.accounts = testrig.NewTestAccounts() + suite.people = testrig.NewTestFediPeople() suite.typeconverter = typeutils.NewConverter(suite.config, suite.db) } @@ -64,7 +55,7 @@ func (suite *InternalToASTestSuite) TearDownTest() { testrig.StandardDBTeardown(suite.db) } -func (suite *InternalToASTestSuite) TestPostAccountToAS() { +func (suite *InternalToASTestSuite) TestAccountToAS() { testAccount := suite.accounts["local_account_1"] // take zork for this test asPerson, err := suite.typeconverter.AccountToAS(testAccount) diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 42ce3f13b..845885ed3 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -24,7 +24,9 @@ import ( "crypto" "crypto/rand" "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "io/ioutil" "net" "net/http" @@ -1047,6 +1049,37 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit } } +// NewTestFediPeople returns a bunch of activity pub Person representations for testing converters and so on. +func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { + new_person_1priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + new_person_1pub := &new_person_1priv.PublicKey + + return map[string]vocab.ActivityStreamsPerson{ + "new_person_1": newPerson( + URLMustParse("https://unknown-instance.com/users/brand_new_person"), + URLMustParse("https://unknown-instance.com/users/brand_new_person/following"), + URLMustParse("https://unknown-instance.com/users/brand_new_person/followers"), + URLMustParse("https://unknown-instance.com/users/brand_new_person/inbox"), + URLMustParse("https://unknown-instance.com/users/brand_new_person/outbox"), + URLMustParse("https://unknown-instance.com/users/brand_new_person/collections/featured"), + "brand_new_person", + "Geoff Brando New Personson", + "hey I'm a new person, your instance hasn't seen me yet uwu", + URLMustParse("https://unknown-instance.com/@brand_new_person"), + true, + URLMustParse("https://unknown-instance.com/users/brand_new_person#main-key"), + new_person_1pub, + URLMustParse("https://unknown-instance.com/media/some_avatar_filename.jpeg"), + "image/jpeg", + URLMustParse("https://unknown-instance.com/media/some_header_filename.jpeg"), + "image/png", + ), + } +} + func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature { sig, digest, date := getSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].URI)) return map[string]ActivityWithSignature{ @@ -1134,6 +1167,186 @@ func getSignatureForDereference(pubKeyID string, privkey crypto.PrivateKey, dest return } +func newPerson( + profileIDURI *url.URL, + followingURI *url.URL, + followersURI *url.URL, + inboxURI *url.URL, + outboxURI *url.URL, + featuredURI *url.URL, + username string, + displayName string, + note string, + profileURL *url.URL, + discoverable bool, + publicKeyURI *url.URL, + pkey *rsa.PublicKey, + avatarURL *url.URL, + avatarContentType string, + headerURL *url.URL, + headerContentType string) vocab.ActivityStreamsPerson { + person := streams.NewActivityStreamsPerson() + + // id should be the activitypub URI of this user + // something like https://example.org/users/example_user + idProp := streams.NewJSONLDIdProperty() + idProp.SetIRI(profileIDURI) + person.SetJSONLDId(idProp) + + // following + // The URI for retrieving a list of accounts this user is following + followingProp := streams.NewActivityStreamsFollowingProperty() + followingProp.SetIRI(followingURI) + person.SetActivityStreamsFollowing(followingProp) + + // followers + // The URI for retrieving a list of this user's followers + followersProp := streams.NewActivityStreamsFollowersProperty() + followersProp.SetIRI(followersURI) + person.SetActivityStreamsFollowers(followersProp) + + // inbox + // the activitypub inbox of this user for accepting messages + inboxProp := streams.NewActivityStreamsInboxProperty() + inboxProp.SetIRI(inboxURI) + person.SetActivityStreamsInbox(inboxProp) + + // outbox + // the activitypub outbox of this user for serving messages + outboxProp := streams.NewActivityStreamsOutboxProperty() + outboxProp.SetIRI(outboxURI) + person.SetActivityStreamsOutbox(outboxProp) + + // featured posts + // Pinned posts. + featuredProp := streams.NewTootFeaturedProperty() + featuredProp.SetIRI(featuredURI) + person.SetTootFeatured(featuredProp) + + // featuredTags + // NOT IMPLEMENTED + + // preferredUsername + // Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI. + preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty() + preferredUsernameProp.SetXMLSchemaString(username) + person.SetActivityStreamsPreferredUsername(preferredUsernameProp) + + // name + // Used as profile display name. + nameProp := streams.NewActivityStreamsNameProperty() + if displayName != "" { + nameProp.AppendXMLSchemaString(displayName) + } else { + nameProp.AppendXMLSchemaString(username) + } + person.SetActivityStreamsName(nameProp) + + // summary + // Used as profile bio. + if note != "" { + summaryProp := streams.NewActivityStreamsSummaryProperty() + summaryProp.AppendXMLSchemaString(note) + person.SetActivityStreamsSummary(summaryProp) + } + + // url + // Used as profile link. + urlProp := streams.NewActivityStreamsUrlProperty() + urlProp.AppendIRI(profileURL) + person.SetActivityStreamsUrl(urlProp) + + // manuallyApprovesFollowers + // Will be shown as a locked account. + // TODO: NOT IMPLEMENTED **YET** -- this needs to be added as an activitypub extension to https://github.com/go-fed/activity, see https://github.com/go-fed/activity/tree/master/astool + + // discoverable + // Will be shown in the profile directory. + discoverableProp := streams.NewTootDiscoverableProperty() + discoverableProp.Set(discoverable) + person.SetTootDiscoverable(discoverableProp) + + // devices + // NOT IMPLEMENTED, probably won't implement + + // alsoKnownAs + // Required for Move activity. + // TODO: NOT IMPLEMENTED **YET** -- this needs to be added as an activitypub extension to https://github.com/go-fed/activity, see https://github.com/go-fed/activity/tree/master/astool + + // publicKey + // Required for signatures. + publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty() + + // create the public key + publicKey := streams.NewW3IDSecurityV1PublicKey() + + // set ID for the public key + publicKeyIDProp := streams.NewJSONLDIdProperty() + publicKeyIDProp.SetIRI(publicKeyURI) + publicKey.SetJSONLDId(publicKeyIDProp) + + // set owner for the public key + publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty() + publicKeyOwnerProp.SetIRI(profileIDURI) + publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp) + + // set the pem key itself + encodedPublicKey, err := x509.MarshalPKIXPublicKey(pkey) + if err != nil { + panic(err) + } + publicKeyBytes := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: encodedPublicKey, + }) + publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty() + publicKeyPEMProp.Set(string(publicKeyBytes)) + publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp) + + // append the public key to the public key property + publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey) + + // set the public key property on the Person + person.SetW3IDSecurityV1PublicKey(publicKeyProp) + + // tag + // TODO: Any tags used in the summary of this profile + + // attachment + // Used for profile fields. + // TODO: The PropertyValue type has to be added: https://schema.org/PropertyValue + + // endpoints + // NOT IMPLEMENTED -- this is for shared inbox which we don't use + + // icon + // Used as profile avatar. + iconProperty := streams.NewActivityStreamsIconProperty() + iconImage := streams.NewActivityStreamsImage() + mediaType := streams.NewActivityStreamsMediaTypeProperty() + mediaType.Set(avatarContentType) + iconImage.SetActivityStreamsMediaType(mediaType) + avatarURLProperty := streams.NewActivityStreamsUrlProperty() + avatarURLProperty.AppendIRI(avatarURL) + iconImage.SetActivityStreamsUrl(avatarURLProperty) + iconProperty.AppendActivityStreamsImage(iconImage) + person.SetActivityStreamsIcon(iconProperty) + + // image + // Used as profile header. + headerProperty := streams.NewActivityStreamsImageProperty() + headerImage := streams.NewActivityStreamsImage() + headerMediaType := streams.NewActivityStreamsMediaTypeProperty() + mediaType.Set(headerContentType) + headerImage.SetActivityStreamsMediaType(headerMediaType) + headerURLProperty := streams.NewActivityStreamsUrlProperty() + headerURLProperty.AppendIRI(headerURL) + headerImage.SetActivityStreamsUrl(headerURLProperty) + headerProperty.AppendActivityStreamsImage(headerImage) + + return person +} + // newNote returns a new activity streams note for the given parameters func newNote( noteID *url.URL,