hup hup hup allee

This commit is contained in:
tsmethurst 2021-05-07 15:10:47 +02:00
parent 9e1f61c373
commit 8104a03bd5
12 changed files with 697 additions and 281 deletions

View File

@ -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()})

View File

@ -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
}

View File

@ -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 := &gtsmodel.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 := &gtsmodel.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
}

View File

@ -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 := &gtsmodel.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 := &gtsmodel.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
}

View File

@ -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
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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")
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 = &gtsmodel.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")
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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))
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@ -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 := &gtsmodel.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

View File

@ -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)

View File

@ -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,