mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-09 23:38:06 -06:00
federate statuses
This commit is contained in:
parent
cc0262055a
commit
a248af1885
14 changed files with 444 additions and 81 deletions
58
internal/api/s2s/user/followers.go
Normal file
58
internal/api/s2s/user/followers.go
Normal 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)
|
||||||
|
}
|
||||||
46
internal/api/s2s/user/statusget.go
Normal file
46
internal/api/s2s/user/statusget.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 := >smodel.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, >smodel.Mention{
|
menchies = append(menchies, >smodel.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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 := >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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 := >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) {
|
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 := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
|
|
|
||||||
|
|
@ -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 := >smodel.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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 := >smodel.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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue