diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index a113ef2..1b71878 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 0000000..de5701f --- /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 d866e47..e1bdb9a 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 b45b01b..fab4907 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 30b073b..01dc714 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 8f203e1..af68590 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 61fecb1..ab4b5cc 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 3f53ed6..6ae0152 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 29fd550..424081c 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 491997b..173da18 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 ffaa1b9..d3ebce4 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 5d02836..41ab285 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 54b2ada..bcd64d4 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 67c96ab..b053f31 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 1c04272..b3e6eb2 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 7eb3f59..dcc2674 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 f1287e0..9d6ce4e 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 ac2ce43..63e201d 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 4891e31..7fbe9eb 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 cee9dcb..0ee4a51 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 +}