From ee65d19ff343134c55ca968114dcbfe4b7b4431d Mon Sep 17 00:00:00 2001 From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com> Date: Sun, 23 May 2021 18:07:04 +0200 Subject: [PATCH] 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. --- cmd/gotosocial/main.go | 32 ++-- internal/api/s2s/user/following.go | 58 ++++++ internal/api/s2s/user/user.go | 3 + internal/api/s2s/user/userget_test.go | 2 +- internal/db/pg/pg.go | 15 ++ internal/federation/federating_db.go | 209 +++++++++++++++++----- internal/federation/federatingprotocol.go | 2 +- internal/federation/util.go | 16 +- internal/message/accountprocess.go | 6 +- internal/message/fediprocess.go | 42 ++++- internal/message/fromfederatorprocess.go | 23 ++- internal/message/frprocess.go | 4 +- internal/message/processor.go | 4 + internal/message/processorutil.go | 12 +- internal/typeutils/asextractionutil.go | 35 ++-- internal/typeutils/astointernal.go | 22 +-- internal/typeutils/astointernal_test.go | 4 +- internal/typeutils/converter.go | 8 +- internal/typeutils/internaltofrontend.go | 6 +- internal/util/uri.go | 22 +++ 20 files changed, 419 insertions(+), 106 deletions(-) create mode 100644 internal/api/s2s/user/following.go diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index a113ef2dc..1b7187809 100644 --- a/cmd/gotosocial/main.go +++ b/cmd/gotosocial/main.go @@ -277,16 +277,16 @@ func main() { Usage: "create a new account", Flags: []cli.Flag{ &cli.StringFlag{ - Name: config.UsernameFlag, - Usage: config.UsernameUsage, + Name: config.UsernameFlag, + Usage: config.UsernameUsage, }, &cli.StringFlag{ - Name: config.EmailFlag, - Usage: config.EmailUsage, + Name: config.EmailFlag, + Usage: config.EmailUsage, }, &cli.StringFlag{ - Name: config.PasswordFlag, - Usage: config.PasswordUsage, + Name: config.PasswordFlag, + Usage: config.PasswordUsage, }, }, Action: func(c *cli.Context) error { @@ -298,8 +298,8 @@ func main() { Usage: "confirm an existing account manually, thereby skipping email confirmation", Flags: []cli.Flag{ &cli.StringFlag{ - Name: config.UsernameFlag, - Usage: config.UsernameUsage, + Name: config.UsernameFlag, + Usage: config.UsernameUsage, }, }, Action: func(c *cli.Context) error { @@ -311,8 +311,8 @@ func main() { Usage: "promote an account to admin", Flags: []cli.Flag{ &cli.StringFlag{ - Name: config.UsernameFlag, - Usage: config.UsernameUsage, + Name: config.UsernameFlag, + Usage: config.UsernameUsage, }, }, Action: func(c *cli.Context) error { @@ -324,8 +324,8 @@ func main() { Usage: "demote an account from admin to normal user", Flags: []cli.Flag{ &cli.StringFlag{ - Name: config.UsernameFlag, - Usage: config.UsernameUsage, + Name: config.UsernameFlag, + Usage: config.UsernameUsage, }, }, 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", Flags: []cli.Flag{ &cli.StringFlag{ - Name: config.UsernameFlag, - Usage: config.UsernameUsage, + Name: config.UsernameFlag, + Usage: config.UsernameUsage, }, }, Action: func(c *cli.Context) error { @@ -350,8 +350,8 @@ func main() { Usage: "completely remove an account and all of its posts, media, etc", Flags: []cli.Flag{ &cli.StringFlag{ - Name: config.UsernameFlag, - Usage: config.UsernameUsage, + Name: config.UsernameFlag, + Usage: config.UsernameUsage, }, }, Action: func(c *cli.Context) error { diff --git a/internal/api/s2s/user/following.go b/internal/api/s2s/user/following.go new file mode 100644 index 000000000..de5701f8d --- /dev/null +++ b/internal/api/s2s/user/following.go @@ -0,0 +1,58 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package user + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +func (m *Module) 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) +} diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go index d866e47e1..e1bdb9a8d 100644 --- a/internal/api/s2s/user/user.go +++ b/internal/api/s2s/user/user.go @@ -44,6 +44,8 @@ const ( 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 + // 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 = 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.MethodPost, UsersInboxPath, m.InboxPOSTHandler) s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler) + s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler) s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler) return nil } diff --git a/internal/api/s2s/user/userget_test.go b/internal/api/s2s/user/userget_test.go index b45b01b63..fab490767 100644 --- a/internal/api/s2s/user/userget_test.go +++ b/internal/api/s2s/user/userget_test.go @@ -145,7 +145,7 @@ func (suite *UserGetTestSuite) TestGetUser() { // convert person to account // 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.EqualValues(suite.T(), targetAccount.Username, a.Username) } diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index 30b073bcc..01dc71434 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -500,6 +500,13 @@ func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuse return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil }) } + if maxID != "" { + s := >smodel.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 == pg.ErrNoRows { return db.ErrNoEntries{} @@ -1113,6 +1120,14 @@ func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID str Limit(limit). Order("status.created_at DESC") + if maxID != "" { + s := >smodel.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() if err != nil { if err != pg.ErrNoRows { diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index 8f203e132..af685904a 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -241,6 +241,40 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { 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, >smodel.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, >smodel.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()) } @@ -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") } + 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) if fromFederatorChanI == nil { 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") } - switch asType.GetTypeName() { - case gtsmodel.ActivityStreamsUpdate: - update, ok := asType.(vocab.ActivityStreamsCreate) - if !ok { - return errors.New("could not convert type to create") - } - object := update.GetActivityStreamsObject() - for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { - 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) - } + typeName := asType.GetTypeName() + if typeName == gtsmodel.ActivityStreamsApplication || + typeName == gtsmodel.ActivityStreamsGroup || + typeName == gtsmodel.ActivityStreamsOrganization || + typeName == gtsmodel.ActivityStreamsPerson || + typeName == gtsmodel.ActivityStreamsService { + // it's an UPDATE to some kind of account + var accountable typeutils.Accountable - fromFederatorChan <- gtsmodel.FromFederator{ - APObjectType: gtsmodel.ActivityStreamsProfile, - APActivityType: gtsmodel.ActivityStreamsUpdate, - GTSModel: updatedAcct, - ReceivingAccount: receivingAcct, - } - - 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, - } + switch asType.GetTypeName() { + case gtsmodel.ActivityStreamsApplication: + l.Debug("got update for APPLICATION") + i, ok := asType.(vocab.ActivityStreamsApplication) + if !ok { + return errors.New("could not convert type to application") } + 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 } @@ -565,7 +633,7 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { // Protocol instead call Update to create a Tombstone. // // 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( logrus.Fields{ "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()) + + 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 := >smodel.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, >smodel.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 := >smodel.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, >smodel.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 } diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 61fecb11a..ab4b5ccbc 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -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) } - a, err := f.typeConverter.ASRepresentationToAccount(person) + a, err := f.typeConverter.ASRepresentationToAccount(person, false) if err != nil { return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err) } diff --git a/internal/federation/util.go b/internal/federation/util.go index 3f53ed6a7..6ae0152df 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -132,9 +132,11 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques var publicKey interface{} var pkOwnerURI *url.URL + requestingRemoteAccount := >smodel.Account{} + requestingLocalAccount := >smodel.Account{} if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) { + // LOCAL ACCOUNT REQUEST // 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) } @@ -143,8 +145,18 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques if err != nil { 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 { - // 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) if err != nil { return nil, fmt.Errorf("transport err: %s", err) diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go index 29fd55034..424081c34 100644 --- a/internal/message/accountprocess.go +++ b/internal/message/accountprocess.go @@ -83,7 +83,7 @@ func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*api if authed.Account != nil { 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) } @@ -295,7 +295,7 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri } // 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 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 - 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 p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err) } diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go index 491997bf2..173da18ee 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -68,7 +68,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht } // convert it to our internal account representation - requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson) + requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson, false) if err != nil { 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 } +func (p *processor) GetFediFollowing(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)) + } + + 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) { // get the account the request is referring to requestedAccount := >smodel.Account{} diff --git a/internal/message/fromfederatorprocess.go b/internal/message/fromfederatorprocess.go index ffaa1b93b..d3ebce400 100644 --- a/internal/message/fromfederatorprocess.go +++ b/internal/message/fromfederatorprocess.go @@ -68,7 +68,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er } 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) } 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") - 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) } if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil { 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 @@ -220,7 +233,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { continue } - targetAccount, err = p.tc.ASRepresentationToAccount(accountable) + targetAccount, err = p.tc.ASRepresentationToAccount(accountable, false) if err != nil { l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err) continue @@ -243,7 +256,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { 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{ "func": "dereferenceAccountFields", "requestingUsername": requestingUsername, @@ -255,7 +268,7 @@ func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requesti } // 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 l.Debugf("error fetching header/avi for account: %s", err) } diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go index 5d02836e6..41ab285c2 100644 --- a/internal/message/frprocess.go +++ b/internal/message/frprocess.go @@ -68,8 +68,8 @@ func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*ap APObjectType: gtsmodel.ActivityStreamsFollow, APActivityType: gtsmodel.ActivityStreamsAccept, GTSModel: follow, - OriginAccount: originAccount, - TargetAccount: targetAccount, + OriginAccount: originAccount, + TargetAccount: targetAccount, } gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID) diff --git a/internal/message/processor.go b/internal/message/processor.go index 54b2ada04..bcd64d47a 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -140,6 +140,10 @@ type Processor interface { // authentication before returning a JSON serializable interface to the caller. 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 // authentication before returning a JSON serializable interface to the caller. GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go index 67c96abe0..b053f31a2 100644 --- a/internal/message/processorutil.go +++ b/internal/message/processorutil.go @@ -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) } - if !repliedStatus.VisibilityAdvanced.Replyable { - return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) + if repliedStatus.VisibilityAdvanced != nil { + 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 @@ -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 // to reflect the creation of these new attachments. -func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport) error { - if targetAccount.AvatarRemoteURL != "" && targetAccount.AvatarMediaAttachmentID == "" { +func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error { + if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) { a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{ RemoteURL: targetAccount.AvatarRemoteURL, Avatar: true, @@ -341,7 +343,7 @@ func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, targetAccount.AvatarMediaAttachmentID = a.ID } - if targetAccount.HeaderRemoteURL != "" && targetAccount.HeaderMediaAttachmentID == "" { + if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) { a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{ RemoteURL: targetAccount.HeaderRemoteURL, Header: true, diff --git a/internal/typeutils/asextractionutil.go b/internal/typeutils/asextractionutil.go index 1c04272e0..b3e6eb2c4 100644 --- a/internal/typeutils/asextractionutil.go +++ b/internal/typeutils/asextractionutil.go @@ -29,7 +29,6 @@ import ( "time" "github.com/go-fed/activity/pub" - "github.com/go-fed/activity/streams" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -63,6 +62,9 @@ func extractName(i withName) (string, error) { func extractInReplyToURI(i withInReplyTo) (*url.URL, error) { 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() { if iter.IsIRI() { if iter.GetIRI() != nil { @@ -76,6 +78,9 @@ func extractInReplyToURI(i withInReplyTo) (*url.URL, error) { func extractTos(i withTo) ([]*url.URL, error) { to := []*url.URL{} toProp := i.GetActivityStreamsTo() + if toProp == nil { + return nil, errors.New("toProp was nil") + } for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() { if iter.IsIRI() { if iter.GetIRI() != nil { @@ -89,6 +94,9 @@ func extractTos(i withTo) ([]*url.URL, error) { func extractCCs(i withCC) ([]*url.URL, error) { cc := []*url.URL{} ccProp := i.GetActivityStreamsCc() + if ccProp == nil { + return cc, nil + } for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() { if iter.IsIRI() { if iter.GetIRI() != nil { @@ -101,6 +109,9 @@ func extractCCs(i withCC) ([]*url.URL, error) { func extractAttributedTo(i withAttributedTo) (*url.URL, error) { attributedToProp := i.GetActivityStreamsAttributedTo() + if attributedToProp == nil { + return nil, errors.New("attributedToProp was nil") + } for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() { if iter.IsIRI() { if iter.GetIRI() != nil { @@ -302,27 +313,21 @@ func extractContent(i withContent) (string, error) { func extractAttachments(i withAttachment) ([]*gtsmodel.MediaAttachment, error) { attachments := []*gtsmodel.MediaAttachment{} - attachmentProp := i.GetActivityStreamsAttachment() + if attachmentProp == nil { + return attachments, nil + } for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() { - t := iter.GetType() if t == nil { - fmt.Printf("\n\n\nGetType() nil\n\n\n") continue } - - m, _ := streams.Serialize(t) - fmt.Printf("\n\n\n%s\n\n\n", m) - attachmentable, ok := t.(Attachmentable) if !ok { - fmt.Printf("\n\n\nnot attachmentable\n\n\n") continue } attachment, err := extractAttachment(attachmentable) if err != nil { - fmt.Printf("\n\n\n%s\n\n\n", err) continue } attachments = append(attachments, attachment) @@ -373,8 +378,10 @@ func extractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) { func extractHashtags(i withTag) ([]*gtsmodel.Tag, error) { tags := []*gtsmodel.Tag{} - tagsProp := i.GetActivityStreamsTag() + if tagsProp == nil { + return tags, nil + } for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { t := iter.GetType() if t == nil { @@ -421,6 +428,9 @@ func extractHashtag(i Hashtaggable) (*gtsmodel.Tag, error) { func extractEmojis(i withTag) ([]*gtsmodel.Emoji, error) { emojis := []*gtsmodel.Emoji{} tagsProp := i.GetActivityStreamsTag() + if tagsProp == nil { + return emojis, nil + } for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { t := iter.GetType() if t == nil { @@ -478,6 +488,9 @@ func extractEmoji(i Emojiable) (*gtsmodel.Emoji, error) { func extractMentions(i withTag) ([]*gtsmodel.Mention, error) { mentions := []*gtsmodel.Mention{} tagsProp := i.GetActivityStreamsTag() + if tagsProp == nil { + return mentions, nil + } for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { t := iter.GetType() if t == nil { diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 7eb3f5927..dcc2674cd 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -28,7 +28,7 @@ import ( "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 uriProp := accountable.GetJSONLDId() if uriProp == nil || !uriProp.IsIRI() { @@ -37,17 +37,19 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode uri := uriProp.GetIRI() acct := >smodel.Account{} - err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct) - if err == 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 - return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err) + if !update { + err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct) + if err == 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 + 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 = >smodel.Account{} acct.URI = uri.String() diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index f1287e027..9d6ce4e0a 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -349,7 +349,7 @@ func (suite *ASToInternalTestSuite) TestParsePerson() { testPerson := suite.people["new_person_1"] - acct, err := suite.typeconverter.ASRepresentationToAccount(testPerson) + acct, err := suite.typeconverter.ASRepresentationToAccount(testPerson, false) assert.NoError(suite.T(), err) fmt.Printf("%+v", acct) @@ -367,7 +367,7 @@ func (suite *ASToInternalTestSuite) TestParseGargron() { rep, ok := t.(typeutils.Accountable) assert.True(suite.T(), ok) - acct, err := suite.typeconverter.ASRepresentationToAccount(rep) + acct, err := suite.typeconverter.ASRepresentationToAccount(rep, false) assert.NoError(suite.T(), err) fmt.Printf("%+v", acct) diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index ac2ce4317..63e201ded 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -95,8 +95,12 @@ type TypeConverter interface { ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL */ - // ASPersonToAccount converts a remote account/person/application representation into a gts model account - ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error) + // ASPersonToAccount converts a remote account/person/application representation into a gts model account. + // + // 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. ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) // ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 4891e31ee..7fbe9eb3f 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -233,9 +233,9 @@ func (c *converter) MentionToMasto(m *gtsmodel.Mention) (model.Mention, error) { var acct string if local { - acct = fmt.Sprintf("@%s", target.Username) + acct = target.Username } else { - acct = fmt.Sprintf("@%s@%s", target.Username, target.Domain) + acct = fmt.Sprintf("%s@%s", target.Username, target.Domain) } return model.Mention{ @@ -567,7 +567,7 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro if i.ContactAccountID != "" { ia := >smodel.Account{} if err := c.db.GetByID(i.ContactAccountID, ia); err == nil { - ma, err := c.AccountToMastoPublic(ia) + ma, err := c.AccountToMastoPublic(ia) if err == nil { mi.ContactAccount = ma } diff --git a/internal/util/uri.go b/internal/util/uri.go index cee9dcbaa..0ee4a5120 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -232,3 +232,25 @@ func ParseOutboxPath(id *url.URL) (username string, err error) { username = matches[1] 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 +}