federate statuses

This commit is contained in:
tsmethurst 2021-05-21 15:46:39 +02:00
commit a248af1885
14 changed files with 444 additions and 81 deletions

View file

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

View file

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

View file

@ -32,6 +32,8 @@ import (
const ( const (
// UsernameKey is for account usernames. // UsernameKey is for account usernames.
UsernameKey = "username" 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 is the base path for serving information about Users eg https://example.org/users
UsersBasePath = "/" + util.UsersPath UsersBasePath = "/" + util.UsersPath
// UsersBasePathWithUsername is just the users base path with the Username key in it. // UsersBasePathWithUsername is just the users base path with the Username key in it.
@ -40,6 +42,10 @@ const (
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
// UsersInboxPath is for serving POST requests to a user's inbox with the given username key. // UsersInboxPath is for serving POST requests to a user's inbox with the given username key.
UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath 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: // 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 { func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler)
s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler)
s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler)
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
return nil return nil
} }

View file

@ -303,7 +303,6 @@ func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error {
q = q.Where("? = ?", pg.Safe(w.Key), w.Value) q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
} }
if _, err := q.Delete(); err != nil { if _, err := q.Delete(); err != nil {
// if there are no rows *anyway* then that's fine // if there are no rows *anyway* then that's fine
// just return err if there's an actual error // 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) { func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) {
ogAccount := &gtsmodel.Account{}
if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil {
return nil, err
}
menchies := []*gtsmodel.Mention{} menchies := []*gtsmodel.Mention{}
for _, a := range targetAccounts { for _, a := range targetAccounts {
// A mentioned account looks like "@test@example.org" or just "@test" for a local account // 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! // id, createdAt and updatedAt will be populated by the db, so we have everything we need!
menchies = append(menchies, &gtsmodel.Mention{ menchies = append(menchies, &gtsmodel.Mention{
StatusID: statusID, StatusID: statusID,
OriginAccountID: originAccountID, OriginAccountID: ogAccount.ID,
TargetAccountID: mentionedAccount.ID, OriginAccountURI: ogAccount.URI,
TargetAccountID: mentionedAccount.ID,
NameString: a,
MentionedAccountURI: mentionedAccount.URI,
GTSAccount: mentionedAccount,
}) })
} }
return menchies, nil return menchies, nil

View file

@ -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 // fallback default behavior: just return a random UUID after our protocol and host

View file

@ -34,6 +34,8 @@ import (
type Federator interface { type Federator interface {
// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes. // FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes.
FederatingActor() pub.FederatingActor 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. // 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. // 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) AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)

View file

@ -27,11 +27,13 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/go-fed/activity/pub" "github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils" "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) return nil, fmt.Errorf("could not parse key id into a url: %s", err)
} }
transport, err := f.GetTransportForUser(username) var publicKey interface{}
if err != nil { var pkOwnerURI *url.URL
return nil, fmt.Errorf("transport err: %s", err) if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) {
} // the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
requestingLocalAccount := &gtsmodel.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. // The actual http call to the remote server is made right here in the Dereference function.
b, err := transport.Dereference(context.Background(), requestingPublicKeyID) b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err) 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 // if the key isn't in the response, we can't authenticate the request
requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID) requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err) 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 // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value") 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 // and decode the PEM so that we can parse it as a golang public key
pubKeyPem := pkPemProp.Get() pubKeyPem := pkPemProp.Get()
block, _ := pem.Decode([]byte(pubKeyPem)) block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" { if block == nil || block.Type != "PUBLIC KEY" {
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
} }
p, err := x509.ParsePKIXPublicKey(block.Bytes) publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err) 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") return nil, errors.New("returned public key was empty")
} }
// do the actual authentication here! // do the actual authentication here!
algo := httpsig.RSA_SHA256 // TODO: make this more robust 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) 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 return pkOwnerURI, nil
} }

View file

@ -38,6 +38,14 @@ type Mention struct {
TargetAccountID string `pg:",notnull"` TargetAccountID string `pg:",notnull"`
// Prevent this mention from generating a notification? // Prevent this mention from generating a notification?
Silent bool 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 // 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: // before the mention is dereferenced. Should be in a form along the lines of:
// @whatever_username@example.org // @whatever_username@example.org
@ -48,4 +56,6 @@ type Mention struct {
// //
// This will not be put in the database, it's just for convenience. // This will not be put in the database, it's just for convenience.
MentionedAccountURI string `pg:"-"` MentionedAccountURI string `pg:"-"`
// A pointer to the gtsmodel account of the mentioned account.
GTSAccount *Account `pg:"-"`
} }

View file

@ -22,6 +22,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@ -122,6 +123,89 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
return data, nil return data, nil
} }
func (p *processor) GetFediFollowers(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))
}
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 := &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))
}
s := &gtsmodel.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) { func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) {
// get the account the request is referring to // get the account the request is referring to
requestedAccount := &gtsmodel.Account{} requestedAccount := &gtsmodel.Account{}

View file

@ -90,30 +90,18 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
} }
func (p *processor) federateStatus(status *gtsmodel.Status) error { func (p *processor) federateStatus(status *gtsmodel.Status) error {
// // derive the sending account -- it might be attached to the status already asStatus, err := p.tc.StatusToAS(status)
// sendingAcct := &gtsmodel.Account{} if err != nil {
// if status.GTSAccount != nil { return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
// 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
// }
// }
// outboxURI, err := url.Parse(sendingAcct.OutboxURI) outboxIRI, err := url.Parse(status.GTSAccount.OutboxURI)
// if err != nil { if err != nil {
// return err return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAccount.OutboxURI, err)
// } }
// // convert the status to AS format Note _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus)
// note, err := p.tc.StatusToAS(status) return err
// if err != nil {
// return err
// }
// _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note)
return nil
} }
func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {

View file

@ -133,6 +133,14 @@ type Processor interface {
// before returning a JSON serializable interface to the caller. // before returning a JSON serializable interface to the caller.
GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) 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 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) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode)

View file

@ -206,7 +206,7 @@ func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, acc
if err := p.db.Put(menchie); err != nil { if err := p.db.Put(menchie); err != nil {
return fmt.Errorf("error putting mentions in db: %s", err) 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 // add full populated gts menchies to the status for passing them around conveniently
status.GTSMentions = gtsMenchies status.GTSMentions = gtsMenchies

View file

@ -117,7 +117,11 @@ type TypeConverter interface {
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation // 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) 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 { type converter struct {

View file

@ -26,6 +26,7 @@ import (
"github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
@ -348,9 +349,10 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e
} }
// tag -- emojis // tag -- emojis
// TODO
// tag -- hashtags // tag -- hashtags
// TODO
status.SetActivityStreamsTag(tagProp) 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) 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 { switch s.Visibility {
case gtsmodel.VisibilityDirect: 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)
}
} }
status.SetActivityStreamsTo(toProp)
// cc status.SetActivityStreamsCc(ccProp)
// conversation // conversation
// TODO
// content // content -- the actual post itself
contentProp := streams.NewActivityStreamsContentProperty()
contentProp.AppendXMLSchemaString(s.Content)
status.SetActivityStreamsContent(contentProp)
// attachment // 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 // replies
// TODO
return nil, nil return status, nil
} }
func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) { 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) { func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) {
mention := streams.NewActivityStreamsMention() if m.GTSAccount == nil {
a := &gtsmodel.Account{}
if m.NameString == "" { if err := c.db.GetWhere([]db.Where{{Key: "target_account_id", Value: m.TargetAccountID}}, a); err != nil {
m.NameString = 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
} }