status deletes, profile updates (#30)

1. Proper DELETE of federated statuses (not yet deleting all the media and stuff -- i still have to implement this -- but the actual status is toast).
2. Proper UPDATE of profiles. When you change your profile picture on your remote instance, that will now register properly in GoToSocial.
3. Scrolling down the home timeline - it no longer just sort of ends, and will keep loading older statuses as you scroll.
4. Little bugfixes -- still had some nil pointer errors when dereferencing remote accounts.
This commit is contained in:
Tobi Smethurst 2021-05-23 18:07:04 +02:00 committed by GitHub
parent aeb665df55
commit ee65d19ff3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 419 additions and 106 deletions

View file

@ -277,16 +277,16 @@ func main() {
Usage: "create a new account", Usage: "create a new account",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: config.UsernameFlag, Name: config.UsernameFlag,
Usage: config.UsernameUsage, Usage: config.UsernameUsage,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: config.EmailFlag, Name: config.EmailFlag,
Usage: config.EmailUsage, Usage: config.EmailUsage,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: config.PasswordFlag, Name: config.PasswordFlag,
Usage: config.PasswordUsage, Usage: config.PasswordUsage,
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -298,8 +298,8 @@ func main() {
Usage: "confirm an existing account manually, thereby skipping email confirmation", Usage: "confirm an existing account manually, thereby skipping email confirmation",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: config.UsernameFlag, Name: config.UsernameFlag,
Usage: config.UsernameUsage, Usage: config.UsernameUsage,
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -311,8 +311,8 @@ func main() {
Usage: "promote an account to admin", Usage: "promote an account to admin",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: config.UsernameFlag, Name: config.UsernameFlag,
Usage: config.UsernameUsage, Usage: config.UsernameUsage,
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -324,8 +324,8 @@ func main() {
Usage: "demote an account from admin to normal user", Usage: "demote an account from admin to normal user",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: config.UsernameFlag, Name: config.UsernameFlag,
Usage: config.UsernameUsage, Usage: config.UsernameUsage,
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -337,8 +337,8 @@ func main() {
Usage: "prevent an account from signing in or posting etc, but don't delete anything", Usage: "prevent an account from signing in or posting etc, but don't delete anything",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: config.UsernameFlag, Name: config.UsernameFlag,
Usage: config.UsernameUsage, Usage: config.UsernameUsage,
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -350,8 +350,8 @@ func main() {
Usage: "completely remove an account and all of its posts, media, etc", Usage: "completely remove an account and all of its posts, media, etc",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: config.UsernameFlag, Name: config.UsernameFlag,
Usage: config.UsernameUsage, Usage: config.UsernameUsage,
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {

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) FollowingGETHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{
"func": "FollowingGETHandler",
"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.GetFediFollowing(requestedUsername, 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, user)
}

View file

@ -44,6 +44,8 @@
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 is for serving GET request's to a user's followers list, with the given username key.
UsersFollowersPath = UsersBasePathWithUsername + "/" + util.FollowersPath UsersFollowersPath = UsersBasePathWithUsername + "/" + util.FollowersPath
// UsersFollowingPath is for serving GET request's to a user's following list, with the given username key.
UsersFollowingPath = UsersBasePathWithUsername + "/" + util.FollowingPath
// UsersStatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID // 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 UsersStatusPath = UsersBasePathWithUsername + "/" + util.StatusesPath + "/:" + StatusIDKey
) )
@ -76,6 +78,7 @@ 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, UsersFollowersPath, m.FollowersGETHandler)
s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler)
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler) s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
return nil return nil
} }

View file

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

View file

@ -500,6 +500,13 @@ func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuse
return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil
}) })
} }
if maxID != "" {
s := &gtsmodel.Status{}
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
return err
}
q = q.Where("status.created_at < ?", s.CreatedAt)
}
if err := q.Select(); err != nil { if err := q.Select(); err != nil {
if err == pg.ErrNoRows { if err == pg.ErrNoRows {
return db.ErrNoEntries{} return db.ErrNoEntries{}
@ -1113,6 +1120,14 @@ func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID str
Limit(limit). Limit(limit).
Order("status.created_at DESC") Order("status.created_at DESC")
if maxID != "" {
s := &gtsmodel.Status{}
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
return nil, err
}
q = q.Where("status.created_at < ?", s.CreatedAt)
}
err := q.Select() err := q.Select()
if err != nil { if err != nil {
if err != pg.ErrNoRows { if err != pg.ErrNoRows {

View file

@ -241,6 +241,40 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
return true, nil return true, nil
} }
if util.IsFollowersPath(id) {
username, err := util.ParseFollowersPath(id)
if err != nil {
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
}
if err := f.db.GetLocalAccountByUsername(username, &gtsmodel.Account{}); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
l.Debug("we DO own this")
return true, nil
}
if util.IsFollowingPath(id) {
username, err := util.ParseFollowingPath(id)
if err != nil {
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
}
if err := f.db.GetLocalAccountByUsername(username, &gtsmodel.Account{}); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
l.Debug("we DO own this")
return true, nil
}
return false, fmt.Errorf("could not match activityID: %s", id.String()) return false, fmt.Errorf("could not match activityID: %s", id.String())
} }
@ -502,6 +536,15 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
l.Error("receiving account was set on context but couldn't be parsed") l.Error("receiving account was set on context but couldn't be parsed")
} }
requestingAcctI := ctx.Value(util.APRequestingAccount)
if receivingAcctI == nil {
l.Error("requesting account wasn't set on context")
}
requestingAcct, ok := requestingAcctI.(*gtsmodel.Account)
if !ok {
l.Error("requesting account was set on context but couldn't be parsed")
}
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
if fromFederatorChanI == nil { if fromFederatorChanI == nil {
l.Error("from federator channel wasn't set on context") l.Error("from federator channel wasn't set on context")
@ -511,51 +554,76 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
l.Error("from federator channel was set on context but couldn't be parsed") l.Error("from federator channel was set on context but couldn't be parsed")
} }
switch asType.GetTypeName() { typeName := asType.GetTypeName()
case gtsmodel.ActivityStreamsUpdate: if typeName == gtsmodel.ActivityStreamsApplication ||
update, ok := asType.(vocab.ActivityStreamsCreate) typeName == gtsmodel.ActivityStreamsGroup ||
if !ok { typeName == gtsmodel.ActivityStreamsOrganization ||
return errors.New("could not convert type to create") typeName == gtsmodel.ActivityStreamsPerson ||
} typeName == gtsmodel.ActivityStreamsService {
object := update.GetActivityStreamsObject() // it's an UPDATE to some kind of account
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { var accountable typeutils.Accountable
switch objectIter.GetType().GetTypeName() {
case string(gtsmodel.ActivityStreamsPerson):
person := objectIter.GetActivityStreamsPerson()
updatedAcct, err := f.typeConverter.ASRepresentationToAccount(person)
if err != nil {
return fmt.Errorf("error converting person to account: %s", err)
}
if err := f.db.Put(updatedAcct); err != nil {
return fmt.Errorf("database error inserting updated account: %s", err)
}
fromFederatorChan <- gtsmodel.FromFederator{ switch asType.GetTypeName() {
APObjectType: gtsmodel.ActivityStreamsProfile, case gtsmodel.ActivityStreamsApplication:
APActivityType: gtsmodel.ActivityStreamsUpdate, l.Debug("got update for APPLICATION")
GTSModel: updatedAcct, i, ok := asType.(vocab.ActivityStreamsApplication)
ReceivingAccount: receivingAcct, if !ok {
} return errors.New("could not convert type to application")
case string(gtsmodel.ActivityStreamsApplication):
application := objectIter.GetActivityStreamsApplication()
updatedAcct, err := f.typeConverter.ASRepresentationToAccount(application)
if err != nil {
return fmt.Errorf("error converting person to account: %s", err)
}
if err := f.db.Put(updatedAcct); err != nil {
return fmt.Errorf("database error inserting updated account: %s", err)
}
fromFederatorChan <- gtsmodel.FromFederator{
APObjectType: gtsmodel.ActivityStreamsProfile,
APActivityType: gtsmodel.ActivityStreamsUpdate,
GTSModel: updatedAcct,
ReceivingAccount: receivingAcct,
}
} }
accountable = i
case gtsmodel.ActivityStreamsGroup:
l.Debug("got update for GROUP")
i, ok := asType.(vocab.ActivityStreamsGroup)
if !ok {
return errors.New("could not convert type to group")
}
accountable = i
case gtsmodel.ActivityStreamsOrganization:
l.Debug("got update for ORGANIZATION")
i, ok := asType.(vocab.ActivityStreamsOrganization)
if !ok {
return errors.New("could not convert type to organization")
}
accountable = i
case gtsmodel.ActivityStreamsPerson:
l.Debug("got update for PERSON")
i, ok := asType.(vocab.ActivityStreamsPerson)
if !ok {
return errors.New("could not convert type to person")
}
accountable = i
case gtsmodel.ActivityStreamsService:
l.Debug("got update for SERVICE")
i, ok := asType.(vocab.ActivityStreamsService)
if !ok {
return errors.New("could not convert type to service")
}
accountable = i
} }
updatedAcct, err := f.typeConverter.ASRepresentationToAccount(accountable, true)
if err != nil {
return fmt.Errorf("error converting to account: %s", err)
}
if requestingAcct.URI != updatedAcct.URI {
return fmt.Errorf("update for account %s was requested by account %s, this is not valid", updatedAcct.URI, requestingAcct.URI)
}
updatedAcct.ID = requestingAcct.ID // set this here so the db will update properly instead of trying to PUT this and getting constraint issues
if err := f.db.UpdateByID(requestingAcct.ID, updatedAcct); err != nil {
return fmt.Errorf("database error inserting updated account: %s", err)
}
fromFederatorChan <- gtsmodel.FromFederator{
APObjectType: gtsmodel.ActivityStreamsProfile,
APActivityType: gtsmodel.ActivityStreamsUpdate,
GTSModel: updatedAcct,
ReceivingAccount: receivingAcct,
}
} }
return nil return nil
} }
@ -565,7 +633,7 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
// Protocol instead call Update to create a Tombstone. // Protocol instead call Update to create a Tombstone.
// //
// The library makes this call only after acquiring a lock first. // The library makes this call only after acquiring a lock first.
func (f *federatingDB) Delete(c context.Context, id *url.URL) error { func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
l := f.log.WithFields( l := f.log.WithFields(
logrus.Fields{ logrus.Fields{
"func": "Delete", "func": "Delete",
@ -573,6 +641,63 @@ func (f *federatingDB) Delete(c context.Context, id *url.URL) error {
}, },
) )
l.Debugf("received DELETE id %s", id.String()) l.Debugf("received DELETE id %s", id.String())
inboxAcctI := ctx.Value(util.APAccount)
if inboxAcctI == nil {
l.Error("inbox account wasn't set on context")
return nil
}
inboxAcct, ok := inboxAcctI.(*gtsmodel.Account)
if !ok {
l.Error("inbox account was set on context but couldn't be parsed")
return nil
}
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
if fromFederatorChanI == nil {
l.Error("from federator channel wasn't set on context")
return nil
}
fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
if !ok {
l.Error("from federator channel was set on context but couldn't be parsed")
return nil
}
// in a delete we only get the URI, we can't know if we have a status or a profile or something else,
// so we have to try a few different things...
where := []db.Where{{Key: "uri", Value: id.String()}}
s := &gtsmodel.Status{}
if err := f.db.GetWhere(where, s); err == nil {
// it's a status
l.Debugf("uri is for status with id: %s", s.ID)
if err := f.db.DeleteByID(s.ID, &gtsmodel.Status{}); err != nil {
return fmt.Errorf("Delete: err deleting status: %s", err)
}
fromFederatorChan <- gtsmodel.FromFederator{
APObjectType: gtsmodel.ActivityStreamsNote,
APActivityType: gtsmodel.ActivityStreamsDelete,
GTSModel: s,
ReceivingAccount: inboxAcct,
}
}
a := &gtsmodel.Account{}
if err := f.db.GetWhere(where, a); err == nil {
// it's an account
l.Debugf("uri is for an account with id: %s", s.ID)
if err := f.db.DeleteByID(a.ID, &gtsmodel.Account{}); err != nil {
return fmt.Errorf("Delete: err deleting account: %s", err)
}
fromFederatorChan <- gtsmodel.FromFederator{
APObjectType: gtsmodel.ActivityStreamsProfile,
APActivityType: gtsmodel.ActivityStreamsDelete,
GTSModel: a,
ReceivingAccount: inboxAcct,
}
}
return nil return nil
} }

View file

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

View file

@ -132,9 +132,11 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
var publicKey interface{} var publicKey interface{}
var pkOwnerURI *url.URL var pkOwnerURI *url.URL
requestingRemoteAccount := &gtsmodel.Account{}
requestingLocalAccount := &gtsmodel.Account{}
if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) { if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) {
// LOCAL ACCOUNT REQUEST
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing // 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 { 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) return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
} }
@ -143,8 +145,18 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err) return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
} }
} else if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingRemoteAccount); err == nil {
// REMOTE ACCOUNT REQUEST WITH KEY CACHED LOCALLY
// this is a remote account and we already have the public key for it so use that
publicKey = requestingRemoteAccount.PublicKey
pkOwnerURI, err = url.Parse(requestingRemoteAccount.URI)
if err != nil {
return nil, fmt.Errorf("error parsing url %s: %s", requestingRemoteAccount.URI, err)
}
} else { } else {
// the request is remote, so we need to authenticate the request properly by dereferencing the remote key // REMOTE ACCOUNT REQUEST WITHOUT KEY CACHED LOCALLY
// the request is remote and we don't have the public key yet,
// so we need to authenticate the request properly by dereferencing the remote key
transport, err := f.GetTransportForUser(username) transport, err := f.GetTransportForUser(username)
if err != nil { if err != nil {
return nil, fmt.Errorf("transport err: %s", err) return nil, fmt.Errorf("transport err: %s", err)

View file

@ -83,7 +83,7 @@ func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*api
if authed.Account != nil { if authed.Account != nil {
requestingUsername = authed.Account.Username requestingUsername = authed.Account.Username
} }
if err := p.dereferenceAccountFields(targetAccount, requestingUsername); err != nil { if err := p.dereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err) p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
} }
@ -295,7 +295,7 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri
} }
// derefence account fields in case we haven't done it already // derefence account fields in case we haven't done it already
if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil { if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
// don't bail if we can't fetch them, we'll try another time // don't bail if we can't fetch them, we'll try another time
p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err) p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
} }
@ -346,7 +346,7 @@ func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID stri
} }
// derefence account fields in case we haven't done it already // derefence account fields in case we haven't done it already
if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil { if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
// don't bail if we can't fetch them, we'll try another time // don't bail if we can't fetch them, we'll try another time
p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err) p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
} }

View file

@ -68,7 +68,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
} }
// convert it to our internal account representation // convert it to our internal account representation
requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson) requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson, false)
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err) return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
} }
@ -163,6 +163,46 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
return data, nil return data, nil
} }
func (p *processor) GetFediFollowing(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))
}
requestedFollowing, err := p.federator.FederatingDB().Following(context.Background(), requestedAccountURI)
if err != nil {
return nil, NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
}
data, err := streams.Serialize(requestedFollowing)
if err != nil {
return nil, NewErrorInternalError(err)
}
return data, nil
}
func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) { func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, 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

@ -68,7 +68,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
} }
l.Debug("will now derefence incoming account") l.Debug("will now derefence incoming account")
if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil { if err := p.dereferenceAccountFields(incomingAccount, "", false); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err) return fmt.Errorf("error dereferencing account from federator: %s", err)
} }
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
@ -86,13 +86,26 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
} }
l.Debug("will now derefence incoming account") l.Debug("will now derefence incoming account")
if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil { if err := p.dereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err) return fmt.Errorf("error dereferencing account from federator: %s", err)
} }
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
return fmt.Errorf("error updating dereferenced account in the db: %s", err) return fmt.Errorf("error updating dereferenced account in the db: %s", err)
} }
} }
case gtsmodel.ActivityStreamsDelete:
// DELETE
switch federatorMsg.APObjectType {
case gtsmodel.ActivityStreamsNote:
// DELETE A STATUS
// TODO: handle side effects of status deletion here:
// 1. delete all media associated with status
// 2. delete boosts of status
// 3. etc etc etc
case gtsmodel.ActivityStreamsProfile:
// DELETE A PROFILE/ACCOUNT
// TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account
}
} }
return nil return nil
@ -220,7 +233,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error {
continue continue
} }
targetAccount, err = p.tc.ASRepresentationToAccount(accountable) targetAccount, err = p.tc.ASRepresentationToAccount(accountable, false)
if err != nil { if err != nil {
l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err) l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err)
continue continue
@ -243,7 +256,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error {
return nil return nil
} }
func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string) error { func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error {
l := p.log.WithFields(logrus.Fields{ l := p.log.WithFields(logrus.Fields{
"func": "dereferenceAccountFields", "func": "dereferenceAccountFields",
"requestingUsername": requestingUsername, "requestingUsername": requestingUsername,
@ -255,7 +268,7 @@ func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requesti
} }
// fetch the header and avatar // fetch the header and avatar
if err := p.fetchHeaderAndAviForAccount(account, t); err != nil { if err := p.fetchHeaderAndAviForAccount(account, t, refresh); err != nil {
// if this doesn't work, just skip it -- we can do it later // if this doesn't work, just skip it -- we can do it later
l.Debugf("error fetching header/avi for account: %s", err) l.Debugf("error fetching header/avi for account: %s", err)
} }

View file

@ -68,8 +68,8 @@ func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*ap
APObjectType: gtsmodel.ActivityStreamsFollow, APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsAccept, APActivityType: gtsmodel.ActivityStreamsAccept,
GTSModel: follow, GTSModel: follow,
OriginAccount: originAccount, OriginAccount: originAccount,
TargetAccount: targetAccount, TargetAccount: targetAccount,
} }
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID) gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)

View file

@ -140,6 +140,10 @@ type Processor interface {
// authentication before returning a JSON serializable interface to the caller. // authentication before returning a JSON serializable interface to the caller.
GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
// GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate // 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. // authentication before returning a JSON serializable interface to the caller.
GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode)

View file

@ -130,8 +130,10 @@ func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, th
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
} }
if !repliedStatus.VisibilityAdvanced.Replyable { if repliedStatus.VisibilityAdvanced != nil {
return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) if !repliedStatus.VisibilityAdvanced.Replyable {
return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
}
} }
// check replied account is known to us // check replied account is known to us
@ -329,8 +331,8 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID
// //
// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated // SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
// to reflect the creation of these new attachments. // to reflect the creation of these new attachments.
func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport) error { func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
if targetAccount.AvatarRemoteURL != "" && targetAccount.AvatarMediaAttachmentID == "" { if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, &gtsmodel.MediaAttachment{ a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, &gtsmodel.MediaAttachment{
RemoteURL: targetAccount.AvatarRemoteURL, RemoteURL: targetAccount.AvatarRemoteURL,
Avatar: true, Avatar: true,
@ -341,7 +343,7 @@ func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account,
targetAccount.AvatarMediaAttachmentID = a.ID targetAccount.AvatarMediaAttachmentID = a.ID
} }
if targetAccount.HeaderRemoteURL != "" && targetAccount.HeaderMediaAttachmentID == "" { if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, &gtsmodel.MediaAttachment{ a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, &gtsmodel.MediaAttachment{
RemoteURL: targetAccount.HeaderRemoteURL, RemoteURL: targetAccount.HeaderRemoteURL,
Header: true, Header: true,

View file

@ -29,7 +29,6 @@
"time" "time"
"github.com/go-fed/activity/pub" "github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
@ -63,6 +62,9 @@ func extractName(i withName) (string, error) {
func extractInReplyToURI(i withInReplyTo) (*url.URL, error) { func extractInReplyToURI(i withInReplyTo) (*url.URL, error) {
inReplyToProp := i.GetActivityStreamsInReplyTo() inReplyToProp := i.GetActivityStreamsInReplyTo()
if inReplyToProp == nil {
return nil, errors.New("in reply to prop was nil")
}
for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() { for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() {
if iter.IsIRI() { if iter.IsIRI() {
if iter.GetIRI() != nil { if iter.GetIRI() != nil {
@ -76,6 +78,9 @@ func extractInReplyToURI(i withInReplyTo) (*url.URL, error) {
func extractTos(i withTo) ([]*url.URL, error) { func extractTos(i withTo) ([]*url.URL, error) {
to := []*url.URL{} to := []*url.URL{}
toProp := i.GetActivityStreamsTo() toProp := i.GetActivityStreamsTo()
if toProp == nil {
return nil, errors.New("toProp was nil")
}
for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() { for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() {
if iter.IsIRI() { if iter.IsIRI() {
if iter.GetIRI() != nil { if iter.GetIRI() != nil {
@ -89,6 +94,9 @@ func extractTos(i withTo) ([]*url.URL, error) {
func extractCCs(i withCC) ([]*url.URL, error) { func extractCCs(i withCC) ([]*url.URL, error) {
cc := []*url.URL{} cc := []*url.URL{}
ccProp := i.GetActivityStreamsCc() ccProp := i.GetActivityStreamsCc()
if ccProp == nil {
return cc, nil
}
for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() { for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() {
if iter.IsIRI() { if iter.IsIRI() {
if iter.GetIRI() != nil { if iter.GetIRI() != nil {
@ -101,6 +109,9 @@ func extractCCs(i withCC) ([]*url.URL, error) {
func extractAttributedTo(i withAttributedTo) (*url.URL, error) { func extractAttributedTo(i withAttributedTo) (*url.URL, error) {
attributedToProp := i.GetActivityStreamsAttributedTo() attributedToProp := i.GetActivityStreamsAttributedTo()
if attributedToProp == nil {
return nil, errors.New("attributedToProp was nil")
}
for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() { for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
if iter.IsIRI() { if iter.IsIRI() {
if iter.GetIRI() != nil { if iter.GetIRI() != nil {
@ -302,27 +313,21 @@ func extractContent(i withContent) (string, error) {
func extractAttachments(i withAttachment) ([]*gtsmodel.MediaAttachment, error) { func extractAttachments(i withAttachment) ([]*gtsmodel.MediaAttachment, error) {
attachments := []*gtsmodel.MediaAttachment{} attachments := []*gtsmodel.MediaAttachment{}
attachmentProp := i.GetActivityStreamsAttachment() attachmentProp := i.GetActivityStreamsAttachment()
if attachmentProp == nil {
return attachments, nil
}
for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() { for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() {
t := iter.GetType() t := iter.GetType()
if t == nil { if t == nil {
fmt.Printf("\n\n\nGetType() nil\n\n\n")
continue continue
} }
m, _ := streams.Serialize(t)
fmt.Printf("\n\n\n%s\n\n\n", m)
attachmentable, ok := t.(Attachmentable) attachmentable, ok := t.(Attachmentable)
if !ok { if !ok {
fmt.Printf("\n\n\nnot attachmentable\n\n\n")
continue continue
} }
attachment, err := extractAttachment(attachmentable) attachment, err := extractAttachment(attachmentable)
if err != nil { if err != nil {
fmt.Printf("\n\n\n%s\n\n\n", err)
continue continue
} }
attachments = append(attachments, attachment) attachments = append(attachments, attachment)
@ -373,8 +378,10 @@ func extractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) {
func extractHashtags(i withTag) ([]*gtsmodel.Tag, error) { func extractHashtags(i withTag) ([]*gtsmodel.Tag, error) {
tags := []*gtsmodel.Tag{} tags := []*gtsmodel.Tag{}
tagsProp := i.GetActivityStreamsTag() tagsProp := i.GetActivityStreamsTag()
if tagsProp == nil {
return tags, nil
}
for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
t := iter.GetType() t := iter.GetType()
if t == nil { if t == nil {
@ -421,6 +428,9 @@ func extractHashtag(i Hashtaggable) (*gtsmodel.Tag, error) {
func extractEmojis(i withTag) ([]*gtsmodel.Emoji, error) { func extractEmojis(i withTag) ([]*gtsmodel.Emoji, error) {
emojis := []*gtsmodel.Emoji{} emojis := []*gtsmodel.Emoji{}
tagsProp := i.GetActivityStreamsTag() tagsProp := i.GetActivityStreamsTag()
if tagsProp == nil {
return emojis, nil
}
for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
t := iter.GetType() t := iter.GetType()
if t == nil { if t == nil {
@ -478,6 +488,9 @@ func extractEmoji(i Emojiable) (*gtsmodel.Emoji, error) {
func extractMentions(i withTag) ([]*gtsmodel.Mention, error) { func extractMentions(i withTag) ([]*gtsmodel.Mention, error) {
mentions := []*gtsmodel.Mention{} mentions := []*gtsmodel.Mention{}
tagsProp := i.GetActivityStreamsTag() tagsProp := i.GetActivityStreamsTag()
if tagsProp == nil {
return mentions, nil
}
for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
t := iter.GetType() t := iter.GetType()
if t == nil { if t == nil {

View file

@ -28,7 +28,7 @@
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
) )
func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error) { func (c *converter) ASRepresentationToAccount(accountable Accountable, update bool) (*gtsmodel.Account, error) {
// first check if we actually already know this account // first check if we actually already know this account
uriProp := accountable.GetJSONLDId() uriProp := accountable.GetJSONLDId()
if uriProp == nil || !uriProp.IsIRI() { if uriProp == nil || !uriProp.IsIRI() {
@ -37,17 +37,19 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
uri := uriProp.GetIRI() uri := uriProp.GetIRI()
acct := &gtsmodel.Account{} acct := &gtsmodel.Account{}
err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct) if !update {
if err == nil { err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct)
// we already know this account so we can skip generating it if err == nil {
return acct, nil // we already know this account so we can skip generating it
} return acct, nil
if _, ok := err.(db.ErrNoEntries); !ok { }
// we don't know the account and there's been a real error if _, ok := err.(db.ErrNoEntries); !ok {
return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err) // we don't know the account and there's been a real error
return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err)
}
} }
// we don't know the account so we need to generate it from the person -- at least we already have the URI! // we don't know the account, or we're being told to update it, so we need to generate it from the person -- at least we already have the URI!
acct = &gtsmodel.Account{} acct = &gtsmodel.Account{}
acct.URI = uri.String() acct.URI = uri.String()

View file

@ -349,7 +349,7 @@ func (suite *ASToInternalTestSuite) TestParsePerson() {
testPerson := suite.people["new_person_1"] testPerson := suite.people["new_person_1"]
acct, err := suite.typeconverter.ASRepresentationToAccount(testPerson) acct, err := suite.typeconverter.ASRepresentationToAccount(testPerson, false)
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
fmt.Printf("%+v", acct) fmt.Printf("%+v", acct)
@ -367,7 +367,7 @@ func (suite *ASToInternalTestSuite) TestParseGargron() {
rep, ok := t.(typeutils.Accountable) rep, ok := t.(typeutils.Accountable)
assert.True(suite.T(), ok) assert.True(suite.T(), ok)
acct, err := suite.typeconverter.ASRepresentationToAccount(rep) acct, err := suite.typeconverter.ASRepresentationToAccount(rep, false)
assert.NoError(suite.T(), err) assert.NoError(suite.T(), err)
fmt.Printf("%+v", acct) fmt.Printf("%+v", acct)

View file

@ -95,8 +95,12 @@ type TypeConverter interface {
ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL
*/ */
// ASPersonToAccount converts a remote account/person/application representation into a gts model account // ASPersonToAccount converts a remote account/person/application representation into a gts model account.
ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error) //
// If update is false, and the account is already known in the database, then the existing account entry will be returned.
// If update is true, then even if the account is already known, all fields in the accountable will be parsed and a new *gtsmodel.Account
// will be generated. This is useful when one needs to force refresh of an account, eg., during an Update of a Profile.
ASRepresentationToAccount(accountable Accountable, update bool) (*gtsmodel.Account, error)
// ASStatus converts a remote activitystreams 'status' representation into a gts model status. // ASStatus converts a remote activitystreams 'status' representation into a gts model status.
ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error)
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. // ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request.

View file

@ -233,9 +233,9 @@ func (c *converter) MentionToMasto(m *gtsmodel.Mention) (model.Mention, error) {
var acct string var acct string
if local { if local {
acct = fmt.Sprintf("@%s", target.Username) acct = target.Username
} else { } else {
acct = fmt.Sprintf("@%s@%s", target.Username, target.Domain) acct = fmt.Sprintf("%s@%s", target.Username, target.Domain)
} }
return model.Mention{ return model.Mention{
@ -567,7 +567,7 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro
if i.ContactAccountID != "" { if i.ContactAccountID != "" {
ia := &gtsmodel.Account{} ia := &gtsmodel.Account{}
if err := c.db.GetByID(i.ContactAccountID, ia); err == nil { if err := c.db.GetByID(i.ContactAccountID, ia); err == nil {
ma, err := c.AccountToMastoPublic(ia) ma, err := c.AccountToMastoPublic(ia)
if err == nil { if err == nil {
mi.ContactAccount = ma mi.ContactAccount = ma
} }

View file

@ -232,3 +232,25 @@ func ParseOutboxPath(id *url.URL) (username string, err error) {
username = matches[1] username = matches[1]
return return
} }
// ParseFollowersPath returns the username from a path such as /users/example_username/followers
func ParseFollowersPath(id *url.URL) (username string, err error) {
matches := followersPathRegex.FindStringSubmatch(id.Path)
if len(matches) != 2 {
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
return
}
username = matches[1]
return
}
// ParseFollowingPath returns the username from a path such as /users/example_username/following
func ParseFollowingPath(id *url.URL) (username string, err error) {
matches := followingPathRegex.FindStringSubmatch(id.Path)
if len(matches) != 2 {
err = fmt.Errorf("expected 2 matches but matches length was %d", len(matches))
return
}
username = matches[1]
return
}