parse various types of actor

This commit is contained in:
tsmethurst 2021-05-07 18:34:03 +02:00
commit 064f961008
11 changed files with 264 additions and 110 deletions

View file

@ -145,7 +145,7 @@ func (suite *UserGetTestSuite) TestGetUser() {
// convert person to account
// since this account is already known, we should get a pretty full model of it from the conversion
a, err := suite.tc.ASPersonToAccount(person)
a, err := suite.tc.ASRepresentationToAccount(person)
assert.NoError(suite.T(), err)
assert.EqualValues(suite.T(), targetAccount.Username, a.Username)
}

View file

@ -130,7 +130,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", publicKeyOwnerURI.String(), err)
}
a, err := f.typeConverter.ASPersonToAccount(person)
a, err := f.typeConverter.ASRepresentationToAccount(person)
if err != nil {
return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err)
}

View file

@ -23,7 +23,6 @@ import (
"net/url"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams/vocab"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -38,9 +37,9 @@ type Federator interface {
// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources.
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)
// DereferenceRemoteAccount can be used to get the ActivityStreamsPerson representation of a remote account, based on the account ID (which is a URI).
// DereferenceRemoteAccount can be used to get the representation of a remote account, based on the account ID (which is a URI).
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (vocab.ActivityStreamsPerson, error)
DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error)
// GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username.
// This can be used for making signed http requests.
GetTransportForUser(username string) (pub.Transport, error)

View file

@ -33,6 +33,7 @@ import (
"github.com/go-fed/activity/streams/vocab"
"github.com/go-fed/httpsig"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
/*
@ -177,7 +178,7 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
return pkOwnerURI, nil
}
func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (vocab.ActivityStreamsPerson, error) {
func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error) {
transport, err := f.GetTransportForUser(username)
if err != nil {

View file

@ -46,7 +46,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
}
// convert it to our internal account representation
requestingAccount, err = p.tc.ASPersonToAccount(requestingPerson)
requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson)
if err != nil {
return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
}

View file

@ -19,30 +19,100 @@
package typeutils
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/url"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/media"
)
type usernameable interface {
// Accountable represents the minimum activitypub interface for representing an 'account'.
// This interface is fulfilled by, for example, vocab.ActivityStreamsPerson and vocab.ActivityStreamsApplication
type Accountable interface {
withJSONLDId
withGetTypeName
withPreferredUsername
withIcon
withDisplayName
withImage
withSummary
withDiscoverable
withURL
withPublicKey
withInbox
withOutbox
withFollowing
withFollowers
withFeatured
}
// all the interfaces below narrow down one particular field of an activity streams object for easy extraction
type withJSONLDId interface {
GetJSONLDId() vocab.JSONLDIdProperty
}
type withGetTypeName interface {
GetTypeName() string
}
type withPreferredUsername interface {
GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
}
type iconable interface {
type withIcon interface {
GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
}
type displaynameable interface {
type withDisplayName interface {
GetActivityStreamsName() vocab.ActivityStreamsNameProperty
}
type imageable interface {
type withImage interface {
GetActivityStreamsImage() vocab.ActivityStreamsImageProperty
}
func extractPreferredUsername(i usernameable) (string, error) {
type withSummary interface {
GetActivityStreamsSummary() vocab.ActivityStreamsSummaryProperty
}
type withDiscoverable interface {
GetTootDiscoverable() vocab.TootDiscoverableProperty
}
type withURL interface {
GetActivityStreamsUrl() vocab.ActivityStreamsUrlProperty
}
type withPublicKey interface {
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
}
type withInbox interface {
GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty
}
type withOutbox interface {
GetActivityStreamsOutbox() vocab.ActivityStreamsOutboxProperty
}
type withFollowing interface {
GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty
}
type withFollowers interface {
GetActivityStreamsFollowers() vocab.ActivityStreamsFollowersProperty
}
type withFeatured interface {
GetTootFeatured() vocab.TootFeaturedProperty
}
func extractPreferredUsername(i withPreferredUsername) (string, error) {
u := i.GetActivityStreamsPreferredUsername()
if u == nil || !u.IsXMLSchemaString() {
return "", errors.New("preferredUsername was not a string")
@ -53,7 +123,7 @@ func extractPreferredUsername(i usernameable) (string, error) {
return u.GetXMLSchemaString(), nil
}
func extractName(i displaynameable) (string, error) {
func extractName(i withDisplayName) (string, error) {
nameProp := i.GetActivityStreamsName()
if nameProp == nil {
return "", errors.New("activityStreamsName not found")
@ -75,7 +145,7 @@ func extractName(i displaynameable) (string, error) {
// "type": "Image",
// "url": "http://example.org/path/to/some/file.jpeg"
// },
func extractIconURL(i iconable) (*url.URL, error) {
func extractIconURL(i withIcon) (*url.URL, error) {
iconProp := i.GetActivityStreamsIcon()
if iconProp == nil {
return nil, errors.New("icon property was nil")
@ -84,8 +154,7 @@ func extractIconURL(i iconable) (*url.URL, error) {
// 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
// 2. 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() {
@ -96,43 +165,23 @@ func extractIconURL(i iconable) (*url.URL, error) {
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
// 2. has a URL so we can grab it
url, err := extractURL(imageValue)
if err == nil && url != nil {
return url, 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) {
func extractImageURL(i withImage) (*url.URL, error) {
imageProp := i.GetActivityStreamsImage()
if imageProp == nil {
return nil, errors.New("icon property was nil")
@ -141,8 +190,7 @@ func extractImageURL(i imageable) (*url.URL, error) {
// 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
// 2. 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() {
@ -153,31 +201,99 @@ func extractImageURL(i imageable) (*url.URL, error) {
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
// 2. has a URL so we can grab it
url, err := extractURL(imageValue)
if err == nil && url != nil {
return url, 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")
}
func extractSummary(i withSummary) (string, error) {
summaryProp := i.GetActivityStreamsSummary()
if summaryProp == nil {
return "", errors.New("summary property was nil")
}
for summaryIter := summaryProp.Begin(); summaryIter != summaryProp.End(); summaryIter = summaryIter.Next() {
if summaryIter.IsXMLSchemaString() && summaryIter.GetXMLSchemaString() != "" {
return summaryIter.GetXMLSchemaString(), nil
}
}
return "", errors.New("could not extract summary")
}
func extractDiscoverable(i withDiscoverable) (bool, error) {
if i.GetTootDiscoverable() == nil {
return false, errors.New("discoverable was nil")
}
return i.GetTootDiscoverable().Get(), nil
}
func extractURL(i withURL) (*url.URL, error) {
urlProp := i.GetActivityStreamsUrl()
if urlProp == nil {
return nil, errors.New("url property was nil")
}
for urlIter := urlProp.Begin(); urlIter != urlProp.End(); urlIter = urlIter.Next() {
if urlIter.IsIRI() && urlIter.GetIRI() != nil {
return urlIter.GetIRI(), nil
}
}
return nil, errors.New("could not extract url")
}
func extractPublicKeyForOwner(i withPublicKey, forOwner *url.URL) (*rsa.PublicKey, *url.URL, error) {
publicKeyProp := i.GetW3IDSecurityV1PublicKey()
if publicKeyProp == nil {
return nil, nil, errors.New("public key property was nil")
}
for publicKeyIter := publicKeyProp.Begin(); publicKeyIter != publicKeyProp.End(); publicKeyIter = publicKeyIter.Next() {
pkey := publicKeyIter.Get()
if pkey == nil {
continue
}
pkeyID, err := pub.GetId(pkey)
if err != nil || pkeyID == nil {
continue
}
if pkey.GetW3IDSecurityV1Owner() == nil || pkey.GetW3IDSecurityV1Owner().Get() == nil || pkey.GetW3IDSecurityV1Owner().Get().String() != forOwner.String() {
continue
}
if pkey.GetW3IDSecurityV1PublicKeyPem() == nil {
continue
}
pkeyPem := pkey.GetW3IDSecurityV1PublicKeyPem().Get()
if pkeyPem == "" {
continue
}
block, _ := pem.Decode([]byte(pkeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return nil, nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
}
p, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
}
if p == nil {
return nil, nil, errors.New("returned public key was empty")
}
if publicKey, ok := p.(*rsa.PublicKey); ok {
return publicKey, pkeyID, nil
}
}
return nil, nil, errors.New("couldn't find public key")
}

View file

@ -22,14 +22,13 @@ import (
"errors"
"fmt"
"github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) {
// first check if we actually already know this person
uriProp := person.GetJSONLDId()
func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error) {
// first check if we actually already know this account
uriProp := accountable.GetJSONLDId()
if uriProp == nil || !uriProp.IsIRI() {
return nil, errors.New("no id property found on person, or id was not an iri")
}
@ -52,7 +51,7 @@ func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsm
// Username aka preferredUsername
// We need this one so bail if it's not set.
username, err := extractPreferredUsername(person)
username, err := extractPreferredUsername(accountable)
if err != nil {
return nil, fmt.Errorf("couldn't extract username: %s", err)
}
@ -63,64 +62,103 @@ func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsm
// avatar aka icon
// if this one isn't extractable in a format we recognise we'll just skip it
if avatarURL, err := extractIconURL(person); err == nil {
if avatarURL, err := extractIconURL(accountable); err == nil {
acct.AvatarRemoteURL = avatarURL.String()
}
// header aka image
// if this one isn't extractable in a format we recognise we'll just skip it
if headerURL, err := extractImageURL(person); err == nil {
if headerURL, err := extractImageURL(accountable); err == nil {
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 {
if displayName, err := extractName(accountable); err == nil {
acct.DisplayName = displayName
}
// fields aka attachment array
// TODO
// TODO: fields aka attachment array
// note aka summary
// TODO
// note aka summary
note, err := extractSummary(accountable)
if err == nil && note != "" {
acct.Note = note
}
// bot
// TODO: parse this from application vs. person type
// check for bot and actor type
switch gtsmodel.ActivityStreamsActor(accountable.GetTypeName()) {
case gtsmodel.ActivityStreamsPerson, gtsmodel.ActivityStreamsGroup, gtsmodel.ActivityStreamsOrganization:
// people, groups, and organizations aren't bots
acct.Bot = false
// apps and services are
case gtsmodel.ActivityStreamsApplication, gtsmodel.ActivityStreamsService:
acct.Bot = true
default:
// we don't know what this is!
return nil, fmt.Errorf("type name %s not recognised or not convertible to gtsmodel.ActivityStreamsActor", accountable.GetTypeName())
}
acct.ActorType = gtsmodel.ActivityStreamsActor(accountable.GetTypeName())
// locked aka manuallyApprovesFollowers
// TODO
// TODO: locked aka manuallyApprovesFollowers
// discoverable
// TODO
// discoverable
// default to false -- take custom value if it's set though
acct.Discoverable = false
discoverable, err := extractDiscoverable(accountable)
if err == nil {
acct.Discoverable = discoverable
}
// url property
// TODO
// url property
url, err := extractURL(accountable)
if err != nil {
return nil, fmt.Errorf("could not extract url for person with id %s: %s", uri.String(), err)
}
acct.URL = url.String()
// InboxURI
// TODO
// InboxURI
if accountable.GetActivityStreamsInbox() == nil || accountable.GetActivityStreamsInbox().GetIRI() == nil {
return nil, fmt.Errorf("person with id %s had no inbox uri", uri.String())
}
acct.InboxURI = accountable.GetActivityStreamsInbox().GetIRI().String()
// OutboxURI
// TODO
// OutboxURI
if accountable.GetActivityStreamsOutbox() == nil || accountable.GetActivityStreamsOutbox().GetIRI() == nil {
return nil, fmt.Errorf("person with id %s had no outbox uri", uri.String())
}
acct.OutboxURI = accountable.GetActivityStreamsOutbox().GetIRI().String()
// FollowingURI
// TODO
if accountable.GetActivityStreamsFollowing() == nil || accountable.GetActivityStreamsFollowing().GetIRI() == nil {
return nil, fmt.Errorf("person with id %s had no following uri", uri.String())
}
acct.FollowingURI = accountable.GetActivityStreamsFollowing().GetIRI().String()
// FollowersURI
// TODO
if accountable.GetActivityStreamsFollowers() == nil || accountable.GetActivityStreamsFollowers().GetIRI() == nil {
return nil, fmt.Errorf("person with id %s had no followers uri", uri.String())
}
acct.FollowersURI = accountable.GetActivityStreamsFollowers().GetIRI().String()
// FeaturedURI
// TODO
// FeaturedURI
// very much optional
if accountable.GetTootFeatured() != nil && accountable.GetTootFeatured().GetIRI() != nil {
acct.FeaturedCollectionURI = accountable.GetTootFeatured().GetIRI().String()
}
// FeaturedTagsURI
// TODO
// TODO: FeaturedTagsURI
// alsoKnownAs
// TODO
// TODO: alsoKnownAs
// publicKey
// TODO
// publicKey
pkey, pkeyURL, err := extractPublicKeyForOwner(accountable, uri)
if err != nil {
return nil, fmt.Errorf("couldn't get public key for person %s: %s", uri.String(), err)
}
acct.PublicKey = pkey
acct.PublicKeyURI = pkeyURL.String()
return acct, nil
}

View file

@ -45,11 +45,11 @@ func (suite *ASToInternalTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db)
}
func (suite *ASToInternalTestSuite) TestASPersonToAccount() {
func (suite *ASToInternalTestSuite) TestASRepresentationToAccount() {
testPerson := suite.people["new_person_1"]
acct, err := suite.typeconverter.ASPersonToAccount(testPerson)
acct, err := suite.typeconverter.ASRepresentationToAccount(testPerson)
assert.NoError(suite.T(), err)
fmt.Printf("%+v", acct)

View file

@ -85,8 +85,8 @@ type TypeConverter interface {
ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL
*/
// ASPersonToAccount converts an activitystreams person into a gts model account
ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error)
// ASPersonToAccount converts a remote account/person/application representation into a gts model account
ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error)
/*
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL

View file

@ -19,7 +19,6 @@
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"
@ -34,7 +33,7 @@ type ConverterStandardTestSuite struct {
db db.DB
log *logrus.Logger
accounts map[string]*gtsmodel.Account
people map[string]vocab.ActivityStreamsPerson
people map[string]typeutils.Accountable
typeconverter typeutils.TypeConverter
}

View file

@ -38,6 +38,7 @@ import (
"github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
// NewTestTokens returns a map of tokens keyed according to which account the token belongs to.
@ -1050,14 +1051,14 @@ 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 {
func NewTestFediPeople() map[string]typeutils.Accountable {
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{
return map[string]typeutils.Accountable{
"new_person_1": newPerson(
URLMustParse("https://unknown-instance.com/users/brand_new_person"),
URLMustParse("https://unknown-instance.com/users/brand_new_person/following"),
@ -1184,7 +1185,7 @@ func newPerson(
avatarURL *url.URL,
avatarContentType string,
headerURL *url.URL,
headerContentType string) vocab.ActivityStreamsPerson {
headerContentType string) typeutils.Accountable {
person := streams.NewActivityStreamsPerson()
// id should be the activitypub URI of this user