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,