From a248af188551d9fa9ff654f320497917460505ee Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Fri, 21 May 2021 15:46:39 +0200 Subject: [PATCH] federate statuses --- internal/api/s2s/user/followers.go | 58 +++++++++ internal/api/s2s/user/statusget.go | 46 +++++++ internal/api/s2s/user/user.go | 8 ++ internal/db/pg/pg.go | 16 ++- internal/federation/federating_db.go | 13 ++ internal/federation/federator.go | 2 + internal/federation/util.go | 92 ++++++++------ internal/gtsmodel/mention.go | 10 ++ internal/message/fediprocess.go | 84 +++++++++++++ internal/message/fromclientapiprocess.go | 32 ++--- internal/message/processor.go | 8 ++ internal/message/processorutil.go | 2 +- internal/typeutils/converter.go | 6 +- internal/typeutils/internaltoas.go | 148 ++++++++++++++++++++--- 14 files changed, 444 insertions(+), 81 deletions(-) create mode 100644 internal/api/s2s/user/followers.go create mode 100644 internal/api/s2s/user/statusget.go diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go new file mode 100644 index 000000000..0b633619f --- /dev/null +++ b/internal/api/s2s/user/followers.go @@ -0,0 +1,58 @@ +/* + 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 user + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +func (m *Module) FollowersGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "FollowersGETHandler", + "url": c.Request.RequestURI, + }) + + requestedUsername := c.Param(UsernameKey) + if requestedUsername == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + return + } + + // make sure this actually an AP request + format := c.NegotiateFormat(ActivityPubAcceptHeaders...) + if format == "" { + c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"}) + return + } + l.Tracef("negotiated format: %s", format) + + // make a copy of the context to pass along so we don't break anything + cp := c.Copy() + user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well + if err != nil { + l.Info(err.Error()) + c.JSON(err.Code(), gin.H{"error": err.Safe()}) + return + } + + c.JSON(http.StatusOK, user) +} diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go new file mode 100644 index 000000000..60efd484e --- /dev/null +++ b/internal/api/s2s/user/statusget.go @@ -0,0 +1,46 @@ +package user + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +func (m *Module) StatusGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "StatusGETHandler", + "url": c.Request.RequestURI, + }) + + requestedUsername := c.Param(UsernameKey) + if requestedUsername == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + return + } + + requestedStatusID := c.Param(StatusIDKey) + if requestedStatusID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"}) + return + } + + // make sure this actually an AP request + format := c.NegotiateFormat(ActivityPubAcceptHeaders...) + if format == "" { + c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"}) + return + } + l.Tracef("negotiated format: %s", format) + + // make a copy of the context to pass along so we don't break anything + cp := c.Copy() + status, err := m.processor.GetFediStatus(requestedUsername, requestedStatusID, cp.Request) // handles auth as well + if err != nil { + l.Info(err.Error()) + c.JSON(err.Code(), gin.H{"error": err.Safe()}) + return + } + + c.JSON(http.StatusOK, status) +} diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go index a6116247d..d866e47e1 100644 --- a/internal/api/s2s/user/user.go +++ b/internal/api/s2s/user/user.go @@ -32,6 +32,8 @@ import ( const ( // UsernameKey is for account usernames. UsernameKey = "username" + // StatusIDKey is for status IDs + StatusIDKey = "status" // UsersBasePath is the base path for serving information about Users eg https://example.org/users UsersBasePath = "/" + util.UsersPath // UsersBasePathWithUsername is just the users base path with the Username key in it. @@ -40,6 +42,10 @@ const ( UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey // UsersInboxPath is for serving POST requests to a user's inbox with the given username key. UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath + // UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key. + UsersFollowersPath = UsersBasePathWithUsername + "/" + util.FollowersPath + // UsersStatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID + UsersStatusPath = UsersBasePathWithUsername + "/" + util.StatusesPath + "/:" + StatusIDKey ) // ActivityPubAcceptHeaders represents the Accept headers mentioned here: @@ -69,5 +75,7 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger) func (m *Module) Route(s router.Router) error { s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) + s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler) + s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler) return nil } diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index db359533e..2a4b040d1 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -303,7 +303,6 @@ func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error { q = q.Where("? = ?", pg.Safe(w.Key), w.Value) } - if _, err := q.Delete(); err != nil { // if there are no rows *anyway* then that's fine // just return err if there's an actual error @@ -1109,6 +1108,11 @@ func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel. */ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { + ogAccount := >smodel.Account{} + if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil { + return nil, err + } + menchies := []*gtsmodel.Mention{} for _, a := range targetAccounts { // A mentioned account looks like "@test@example.org" or just "@test" for a local account @@ -1166,9 +1170,13 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori // id, createdAt and updatedAt will be populated by the db, so we have everything we need! menchies = append(menchies, >smodel.Mention{ - StatusID: statusID, - OriginAccountID: originAccountID, - TargetAccountID: mentionedAccount.ID, + StatusID: statusID, + OriginAccountID: ogAccount.ID, + OriginAccountURI: ogAccount.URI, + TargetAccountID: mentionedAccount.ID, + NameString: a, + MentionedAccountURI: mentionedAccount.URI, + GTSAccount: mentionedAccount, }) } return menchies, nil diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index ce2e5e75d..8f203e132 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -657,6 +657,19 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err } } } + case gtsmodel.ActivityStreamsNote: + // NOTE aka STATUS + // ID might already be set on a note we've created, so check it here and return it if it is + note, ok := t.(vocab.ActivityStreamsNote) + if !ok { + return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsNote") + } + idProp := note.GetJSONLDId() + if idProp != nil { + if idProp.IsIRI() { + return idProp.GetIRI(), nil + } + } } // fallback default behavior: just return a random UUID after our protocol and host diff --git a/internal/federation/federator.go b/internal/federation/federator.go index c1cf21ab1..f09a77279 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -34,6 +34,8 @@ import ( type Federator interface { // FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes. FederatingActor() pub.FederatingActor + // FederatingDB returns the underlying FederatingDB interface. + FederatingDB() FederatingDB // 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) diff --git a/internal/federation/util.go b/internal/federation/util.go index 3d0aa7878..3f53ed6a7 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -27,11 +27,13 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/go-fed/activity/pub" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "github.com/go-fed/httpsig" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/typeutils" @@ -128,57 +130,73 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques return nil, fmt.Errorf("could not parse key id into a url: %s", err) } - transport, err := f.GetTransportForUser(username) - if err != nil { - return nil, fmt.Errorf("transport err: %s", err) - } + var publicKey interface{} + var pkOwnerURI *url.URL + if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) { + // the request is coming from INSIDE THE HOUSE so skip the remote dereferencing + requestingLocalAccount := >smodel.Account{} + if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil { + return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err) + } + publicKey = requestingLocalAccount.PublicKey + pkOwnerURI, err = url.Parse(requestingLocalAccount.URI) + if err != nil { + return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err) + } + } else { + // the request is remote, so we need to authenticate the request properly by dereferencing the remote key + transport, err := f.GetTransportForUser(username) + if err != nil { + return nil, fmt.Errorf("transport err: %s", err) + } - // The actual http call to the remote server is made right here in the Dereference function. - b, err := transport.Dereference(context.Background(), requestingPublicKeyID) - if err != nil { - return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err) - } + // The actual http call to the remote server is made right here in the Dereference function. + b, err := transport.Dereference(context.Background(), requestingPublicKeyID) + if err != nil { + return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err) + } - // if the key isn't in the response, we can't authenticate the request - requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID) - if err != nil { - return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err) - } + // if the key isn't in the response, we can't authenticate the request + requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID) + if err != nil { + return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err) + } - // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey - pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() - if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { - return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value") - } + // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey + pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() + if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { + return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value") + } - // and decode the PEM so that we can parse it as a golang public key - pubKeyPem := pkPemProp.Get() - block, _ := pem.Decode([]byte(pubKeyPem)) - if block == nil || block.Type != "PUBLIC KEY" { - return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") - } + // and decode the PEM so that we can parse it as a golang public key + pubKeyPem := pkPemProp.Get() + block, _ := pem.Decode([]byte(pubKeyPem)) + if block == nil || block.Type != "PUBLIC KEY" { + return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") + } - p, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("could not parse public key from block bytes: %s", err) + publicKey, err = x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("could not parse public key from block bytes: %s", err) + } + + // all good! we just need the URI of the key owner to return + pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() + if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { + return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value") + } + pkOwnerURI = pkOwnerProp.GetIRI() } - if p == nil { + if publicKey == nil { return nil, errors.New("returned public key was empty") } // do the actual authentication here! algo := httpsig.RSA_SHA256 // TODO: make this more robust - if err := verifier.Verify(p, algo); err != nil { + if err := verifier.Verify(publicKey, algo); err != nil { return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err) } - // all good! we just need the URI of the key owner to return - pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() - if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { - return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value") - } - pkOwnerURI := pkOwnerProp.GetIRI() - return pkOwnerURI, nil } diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 8e56a1b36..3abe9c915 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -38,6 +38,14 @@ type Mention struct { TargetAccountID string `pg:",notnull"` // Prevent this mention from generating a notification? Silent bool + + /* + NON-DATABASE CONVENIENCE FIELDS + These fields are just for convenience while passing the mention + around internally, to make fewer database calls and whatnot. They're + not meant to be put in the database! + */ + // NameString is for putting in the namestring of the mentioned user // before the mention is dereferenced. Should be in a form along the lines of: // @whatever_username@example.org @@ -48,4 +56,6 @@ type Mention struct { // // This will not be put in the database, it's just for convenience. MentionedAccountURI string `pg:"-"` + // A pointer to the gtsmodel account of the mentioned account. + GTSAccount *Account `pg:"-"` } diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go index e782fef2c..eb6e8b6d6 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "net/http" + "net/url" "github.com/go-fed/activity/streams" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -122,6 +123,89 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request) return data, nil } +func (p *processor) GetFediFollowers(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)) + } + + requestedAccountURI, err := url.Parse(requestedAccount.URI) + if err != nil { + return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) + } + + requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI) + if err != nil { + return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) + } + + data, err := streams.Serialize(requestedFollowers) + if err != nil { + return nil, NewErrorInternalError(err) + } + + return data, nil +} + +func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID 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)) + } + + s := >smodel.Status{} + if err := p.db.GetWhere([]db.Where{ + {Key: "id", Value: requestedStatusID}, + {Key: "account_id", Value: requestedAccount.ID}, + }, s); err != nil { + return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) + } + + asStatus, err := p.tc.StatusToAS(s) + if err != nil { + return nil, NewErrorInternalError(err) + } + + data, err := streams.Serialize(asStatus) + if err != nil { + return nil, NewErrorInternalError(err) + } + + return data, nil +} + func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) { // get the account the request is referring to requestedAccount := >smodel.Account{} diff --git a/internal/message/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go index f97c9934b..e91bd6ce4 100644 --- a/internal/message/fromclientapiprocess.go +++ b/internal/message/fromclientapiprocess.go @@ -90,30 +90,18 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error } func (p *processor) federateStatus(status *gtsmodel.Status) error { - // // derive the sending account -- it might be attached to the status already - // sendingAcct := >smodel.Account{} - // if status.GTSAccount != nil { - // sendingAcct = status.GTSAccount - // } else { - // // it wasn't attached so get it from the db instead - // if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil { - // return err - // } - // } + asStatus, err := p.tc.StatusToAS(status) + if err != nil { + return fmt.Errorf("federateStatus: error converting status to as format: %s", err) + } - // outboxURI, err := url.Parse(sendingAcct.OutboxURI) - // if err != nil { - // return err - // } + outboxIRI, err := url.Parse(status.GTSAccount.OutboxURI) + if err != nil { + return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAccount.OutboxURI, err) + } - // // convert the status to AS format Note - // note, err := p.tc.StatusToAS(status) - // if err != nil { - // return err - // } - - // _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note) - return nil + _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus) + return err } func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { diff --git a/internal/message/processor.go b/internal/message/processor.go index 2b41aa7b3..e9888d647 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -133,6 +133,14 @@ type Processor interface { // before returning a JSON serializable interface to the caller. GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) + // GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate + // authentication before returning a JSON serializable interface to the caller. + GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) + + // GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate + // authentication before returning a JSON serializable interface to the caller. + GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) + // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go index db0d63875..67c96abe0 100644 --- a/internal/message/processorutil.go +++ b/internal/message/processorutil.go @@ -206,7 +206,7 @@ func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, acc if err := p.db.Put(menchie); err != nil { return fmt.Errorf("error putting mentions in db: %s", err) } - menchies = append(menchies, menchie.TargetAccountID) + menchies = append(menchies, menchie.ID) } // add full populated gts menchies to the status for passing them around conveniently status.GTSMentions = gtsMenchies diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index aad5b3e2e..ac2ce4317 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -117,7 +117,11 @@ type TypeConverter interface { // FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) - MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) + // MentionToAS converts a gts model mention into an activity streams Mention, suitable for federation + MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) + + // AttachmentToAS converts a gts model media attachment into an activity streams Attachment, suitable for federation + AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) } type converter struct { diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index fb8d60d7c..072c4e690 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -26,6 +26,7 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -348,9 +349,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e } // tag -- emojis + // TODO // tag -- hashtags - + // TODO status.SetActivityStreamsTag(tagProp) @@ -365,27 +367,80 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", asPublicURI, err) } - // to + // to and cc + toProp := streams.NewActivityStreamsToProperty() + ccProp := streams.NewActivityStreamsCcProperty() switch s.Visibility { case gtsmodel.VisibilityDirect: + // if DIRECT, then only mentioned users should be added to TO, and nothing to CC + for _, m := range s.GTSMentions { + iri, err := url.Parse(m.GTSAccount.URI) + if err != nil { + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + } + toProp.AppendIRI(iri) + } + case gtsmodel.VisibilityMutualsOnly: + // TODO + case gtsmodel.VisibilityFollowersOnly: + // if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC + toProp.AppendIRI(authorFollowersURI) + for _, m := range s.GTSMentions { + iri, err := url.Parse(m.GTSAccount.URI) + if err != nil { + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + } + ccProp.AppendIRI(iri) + } + case gtsmodel.VisibilityUnlocked: + // if UNLOCKED, we want to add followers to TO, and public and mentions to CC + toProp.AppendIRI(authorFollowersURI) + ccProp.AppendIRI(publicURI) + for _, m := range s.GTSMentions { + iri, err := url.Parse(m.GTSAccount.URI) + if err != nil { + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + } + ccProp.AppendIRI(iri) + } + case gtsmodel.VisibilityPublic: + // if PUBLIC, we want to add public to TO, and followers and mentions to CC + toProp.AppendIRI(publicURI) + ccProp.AppendIRI(authorFollowersURI) + for _, m := range s.GTSMentions { + iri, err := url.Parse(m.GTSAccount.URI) + if err != nil { + return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + } + ccProp.AppendIRI(iri) + } } - - // cc + status.SetActivityStreamsTo(toProp) + status.SetActivityStreamsCc(ccProp) // conversation + // TODO - // content + // content -- the actual post itself + contentProp := streams.NewActivityStreamsContentProperty() + contentProp.AppendXMLSchemaString(s.Content) + status.SetActivityStreamsContent(contentProp) // attachment - - - - + attachmentProp := streams.NewActivityStreamsAttachmentProperty() + for _, a := range s.GTSMediaAttachments { + doc, err := c.AttachmentToAS(a) + if err != nil { + return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err) + } + attachmentProp.AppendActivityStreamsDocument(doc) + } + status.SetActivityStreamsAttachment(attachmentProp) // replies - - - return nil, nil + // TODO + + return status, nil } func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) { @@ -435,11 +490,72 @@ func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Accou } func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) { - mention := streams.NewActivityStreamsMention() - - if m.NameString == "" { - m.NameString = + if m.GTSAccount == nil { + a := >smodel.Account{} + if err := c.db.GetWhere([]db.Where{{Key: "target_account_id", Value: m.TargetAccountID}}, a); err != nil { + return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err) + } + m.GTSAccount = a } + // create the mention + mention := streams.NewActivityStreamsMention() + // href -- this should be the URI of the mentioned user + hrefProp := streams.NewActivityStreamsHrefProperty() + hrefURI, err := url.Parse(m.GTSAccount.URI) + if err != nil { + return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.GTSAccount.URI, err) + } + hrefProp.SetIRI(hrefURI) + mention.SetActivityStreamsHref(hrefProp) + + // name -- this should be the namestring of the mentioned user, something like @whatever@example.org + var domain string + if m.GTSAccount.Domain == "" { + domain = c.config.Host + } else { + domain = m.GTSAccount.Domain + } + username := m.GTSAccount.Username + nameString := fmt.Sprintf("@%s@%s", username, domain) + nameProp := streams.NewActivityStreamsNameProperty() + nameProp.AppendXMLSchemaString(nameString) + mention.SetActivityStreamsName(nameProp) + + return mention, nil +} + +func (c *converter) AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) { + // type -- Document + doc := streams.NewActivityStreamsDocument() + + // mediaType aka mime content type + mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty() + mediaTypeProp.Set(a.File.ContentType) + doc.SetActivityStreamsMediaType(mediaTypeProp) + + // url -- for the original image not the thumbnail + urlProp := streams.NewActivityStreamsUrlProperty() + imageURL, err := url.Parse(a.URL) + if err != nil { + return nil, fmt.Errorf("AttachmentToAS: error parsing uri %s: %s", a.URL, err) + } + urlProp.AppendIRI(imageURL) + doc.SetActivityStreamsUrl(urlProp) + + // name -- aka image description + nameProp := streams.NewActivityStreamsNameProperty() + nameProp.AppendXMLSchemaString(a.Description) + doc.SetActivityStreamsName(nameProp) + + // blurhash + blurProp := streams.NewTootBlurhashProperty() + blurProp.Set(a.Blurhash) + doc.SetTootBlurhash(blurProp) + + // focalpoint + // TODO + + return doc, nil }