From 3623205fd73f38176d6e6b77f9f58452750b7b5d Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Thu, 20 May 2021 14:56:03 +0200 Subject: [PATCH] unfollows from gts => remote now working --- PROGRESS.md | 6 +- internal/api/client/account/account.go | 6 + internal/api/client/account/following.go | 49 +++++++ internal/api/client/account/unfollow.go | 53 +++++++ internal/api/client/auth/authorize.go | 3 +- internal/api/client/auth/middleware.go | 3 +- internal/api/client/auth/signin.go | 3 +- .../api/client/status/statuscreate_test.go | 3 +- internal/db/db.go | 8 +- internal/db/pg/pg.go | 27 +++- internal/federation/federating_db.go | 20 +-- internal/federation/federatingprotocol.go | 4 +- internal/federation/util.go | 6 + internal/message/accountprocess.go | 133 ++++++++++++++++++ internal/message/fediprocess.go | 2 +- internal/message/fromclientapiprocess.go | 48 ++++++- internal/message/fromfederatorprocess.go | 4 +- internal/message/instanceprocess.go | 3 +- internal/message/processor.go | 8 +- internal/oauth/tokenstore.go | 12 +- internal/typeutils/astointernal.go | 14 +- 21 files changed, 368 insertions(+), 47 deletions(-) create mode 100644 internal/api/client/account/following.go create mode 100644 internal/api/client/account/unfollow.go diff --git a/PROGRESS.md b/PROGRESS.md index 95bb3fa8c..653f2df23 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -17,12 +17,12 @@ * [x] /api/v1/accounts/:id GET (Get account information) * [x] /api/v1/accounts/:id/statuses GET (Get an account's statuses) * [x] /api/v1/accounts/:id/followers GET (Get an account's followers) - * [ ] /api/v1/accounts/:id/following GET (Get an account's following) + * [x] /api/v1/accounts/:id/following GET (Get an account's following) * [ ] /api/v1/accounts/:id/featured_tags GET (Get an account's featured tags) * [ ] /api/v1/accounts/:id/lists GET (Get lists containing this account) * [ ] /api/v1/accounts/:id/identity_proofs GET (Get identity proofs for this account) - * [ ] /api/v1/accounts/:id/follow POST (Follow this account) - * [ ] /api/v1/accounts/:id/unfollow POST (Unfollow this account) + * [x] /api/v1/accounts/:id/follow POST (Follow this account) + * [x] /api/v1/accounts/:id/unfollow POST (Unfollow this account) * [ ] /api/v1/accounts/:id/block POST (Block this account) * [ ] /api/v1/accounts/:id/unblock POST (Unblock this account) * [ ] /api/v1/accounts/:id/mute POST (Mute this account) diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go index e9ed76397..94f753825 100644 --- a/internal/api/client/account/account.go +++ b/internal/api/client/account/account.go @@ -57,10 +57,14 @@ const ( GetStatusesPath = BasePathWithID + "/statuses" // GetFollowersPath is for showing an account's followers GetFollowersPath = BasePathWithID + "/followers" + // GetFollowingPath is for showing account's that an account follows. + GetFollowingPath = BasePathWithID + "/following" // GetRelationshipsPath is for showing an account's relationship with other accounts GetRelationshipsPath = BasePath + "/relationships" // FollowPath is for POSTing new follows to, and updating existing follows PostFollowPath = BasePathWithID + "/follow" + // PostUnfollowPath is for POSTing an unfollow + PostUnfollowPath = BasePathWithID + "/unfollow" ) // Module implements the ClientAPIModule interface for account-related actions @@ -86,8 +90,10 @@ func (m *Module) Route(r router.Router) error { r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) + r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler) r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler) + r.AttachHandler(http.MethodPost, PostUnfollowPath, m.AccountUnfollowPOSTHandler) return nil } diff --git a/internal/api/client/account/following.go b/internal/api/client/account/following.go new file mode 100644 index 000000000..2a1373e40 --- /dev/null +++ b/internal/api/client/account/following.go @@ -0,0 +1,49 @@ +/* + 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 account + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// AccountFollowersGETHandler serves the followers of the requested account, if they're visible to the requester. +func (m *Module) AccountFollowingGETHandler(c *gin.Context) { + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + targetAcctID := c.Param(IDKey) + if targetAcctID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + return + } + + following, errWithCode := m.processor.AccountFollowingGet(authed, targetAcctID) + if errWithCode != nil { + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, following) +} diff --git a/internal/api/client/account/unfollow.go b/internal/api/client/account/unfollow.go new file mode 100644 index 000000000..69ed72b88 --- /dev/null +++ b/internal/api/client/account/unfollow.go @@ -0,0 +1,53 @@ +/* + 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 account + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// AccountUnfollowPOSTHandler is the endpoint for removing a follow and/or follow request to the target account +func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { + l := m.log.WithField("func", "AccountUnfollowPOSTHandler") + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debug(err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + targetAcctID := c.Param(IDKey) + if targetAcctID == "" { + l.Debug(err) + c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) + return + } + + relationship, errWithCode := m.processor.AccountFollowRemove(authed, targetAcctID) + if errWithCode != nil { + l.Debug(errWithCode.Error()) + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, relationship) +} diff --git a/internal/api/client/auth/authorize.go b/internal/api/client/auth/authorize.go index d5f8ee214..f473579db 100644 --- a/internal/api/client/auth/authorize.go +++ b/internal/api/client/auth/authorize.go @@ -28,6 +28,7 @@ import ( "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -60,7 +61,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { app := >smodel.Application{ ClientID: clientID, } - if err := m.db.GetWhere("client_id", app.ClientID, app); err != nil { + if err := m.db.GetWhere([]db.Where{{Key: "client_id", Value: app.ClientID}}, app); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)}) return } diff --git a/internal/api/client/auth/middleware.go b/internal/api/client/auth/middleware.go index 2a63cbdb6..dba8e5a1d 100644 --- a/internal/api/client/auth/middleware.go +++ b/internal/api/client/auth/middleware.go @@ -20,6 +20,7 @@ package auth import ( "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -68,7 +69,7 @@ func (m *Module) OauthTokenMiddleware(c *gin.Context) { if cid := ti.GetClientID(); cid != "" { l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope()) app := >smodel.Application{} - if err := m.db.GetWhere("client_id", cid, app); err != nil { + if err := m.db.GetWhere([]db.Where{{Key: "client_id",Value: cid}}, app); err != nil { l.Tracef("no app found for client %s", cid) } c.Set(oauth.SessionAuthorizedApplication, app) diff --git a/internal/api/client/auth/signin.go b/internal/api/client/auth/signin.go index 79d9b300e..e9385e39a 100644 --- a/internal/api/client/auth/signin.go +++ b/internal/api/client/auth/signin.go @@ -24,6 +24,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "golang.org/x/crypto/bcrypt" ) @@ -87,7 +88,7 @@ func (m *Module) ValidatePassword(email string, password string) (userid string, // first we select the user from the database based on email address, bail if no user found for that email gtsUser := >smodel.User{} - if err := m.db.GetWhere("email", email, gtsUser); err != nil { + if err := m.db.GetWhere([]db.Where{{Key: "email", Value: email}}, gtsUser); err != nil { l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) return incorrectPassword() } diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go index fb9b48f8a..a78374fe8 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -32,6 +32,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/status" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" @@ -118,7 +119,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { }, statusReply.Tags[0]) gtsTag := >smodel.Tag{} - err = suite.db.GetWhere("name", "helloworld", gtsTag) + err = suite.db.GetWhere([]db.Where{{Key: "name", Value: "helloworld"}}, gtsTag) assert.NoError(suite.T(), err) assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID) } diff --git a/internal/db/db.go b/internal/db/db.go index c1a252ada..5609b926f 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -42,6 +42,10 @@ func (e ErrAlreadyExists) Error() string { return "already exists" } +type Where struct { + Key string + Value interface{} +} // DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres). // Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated @@ -80,7 +84,7 @@ type DB interface { // name of the key to select from. // The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. // In case of no entries, a 'no entries' error will be returned - GetWhere(key string, value interface{}, i interface{}) error + GetWhere(where []Where, i interface{}) error // // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where". // // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second @@ -114,7 +118,7 @@ type DB interface { // DeleteWhere deletes i where key = value // If i didn't exist anyway, then no error should be returned. - DeleteWhere(key string, value interface{}, i interface{}) error + DeleteWhere(where []Where, i interface{}) error /* HANDY SHORTCUTS diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index aeb011592..db359533e 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -216,8 +216,17 @@ func (ps *postgresService) GetByID(id string, i interface{}) error { return nil } -func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}) error { - if err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Select(); err != nil { +func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error { + if len(where) == 0 { + return errors.New("no queries provided") + } + + q := ps.conn.Model(i) + for _, w := range where { + q = q.Where("? = ?", pg.Safe(w.Key), w.Value) + } + + if err := q.Select(); err != nil { if err == pg.ErrNoRows { return db.ErrNoEntries{} } @@ -284,8 +293,18 @@ func (ps *postgresService) DeleteByID(id string, i interface{}) error { return nil } -func (ps *postgresService) DeleteWhere(key string, value interface{}, i interface{}) error { - if _, err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Delete(); err != nil { +func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error { + if len(where) == 0 { + return errors.New("no queries provided") + } + + q := ps.conn.Model(i) + for _, w := range where { + q = q.Where("? = ?", pg.Safe(w.Key), w.Value) + } + + + if _, err := q.Delete(); err != nil { // if there are no rows *anyway* then that's fine // just return err if there's an actual error if err != pg.ErrNoRows { diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go index 8c5d327fd..ce2e5e75d 100644 --- a/internal/federation/federating_db.go +++ b/internal/federation/federating_db.go @@ -211,7 +211,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { if err != nil { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } - if err := f.db.GetWhere("uri", uid, >smodel.Status{}); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, >smodel.Status{}); err != nil { if _, ok := err.(db.ErrNoEntries); ok { // there are no entries for this status return false, nil @@ -260,7 +260,7 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) } acct := >smodel.Account{} - if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil { if _, ok := err.(db.ErrNoEntries); ok { return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String()) } @@ -285,7 +285,7 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) } acct := >smodel.Account{} - if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { if _, ok := err.(db.ErrNoEntries); ok { return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) } @@ -311,7 +311,7 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) } acct := >smodel.Account{} - if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil { if _, ok := err.(db.ErrNoEntries); ok { return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String()) } @@ -350,7 +350,7 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er if util.IsUserPath(id) { acct := >smodel.Account{} - if err := f.db.GetWhere("uri", id.String(), acct); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil { return nil, err } l.Debug("is user path! returning account") @@ -651,7 +651,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err // take the IRI of the first actor we can find (there should only be one) if iter.IsIRI() { actorAccount := >smodel.Account{} - if err := f.db.GetWhere("uri", iter.GetIRI().String(), actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host)) } } @@ -679,7 +679,7 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String()) acct := >smodel.Account{} - if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) } @@ -721,7 +721,7 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followin l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String()) acct := >smodel.Account{} - if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil { return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err) } @@ -823,11 +823,11 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) return errors.New("UNDO: follow object account and inbox account were not the same") } // delete any existing FOLLOW - if err := f.db.DeleteWhere("uri", gtsFollow.URI, >smodel.Follow{}); err != nil { + if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.Follow{}); err != nil { return fmt.Errorf("UNDO: db error removing follow: %s", err) } // delete any existing FOLLOW REQUEST - if err := f.db.DeleteWhere("uri", gtsFollow.URI, >smodel.FollowRequest{}); err != nil { + if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.FollowRequest{}); err != nil { return fmt.Errorf("UNDO: db error removing follow request: %s", err) } l.Debug("follow undone") diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index b4db31a7e..61fecb11a 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -124,7 +124,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr } requestingAccount := >smodel.Account{} - if err := f.db.GetWhere("uri", publicKeyOwnerURI.String(), requestingAccount); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: publicKeyOwnerURI.String()}}, requestingAccount); err != nil { // there's been a proper error so return it if _, ok := err.(db.ErrNoEntries); !ok { return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err) @@ -200,7 +200,7 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er for _, uri := range actorIRIs { a := >smodel.Account{} - if err := f.db.GetWhere("uri", uri.String(), a); err != nil { + if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil { _, ok := err.(db.ErrNoEntries) if ok { // we don't have an entry for this account so it's not blocked diff --git a/internal/federation/util.go b/internal/federation/util.go index 6949d27c2..3d0aa7878 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -217,6 +217,12 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u return nil, errors.New("error resolving type as activitystreams application") } return p, nil + case string(gtsmodel.ActivityStreamsService): + p, ok := t.(vocab.ActivityStreamsService) + if !ok { + return nil, errors.New("error resolving type as activitystreams service") + } + return p, nil } return nil, fmt.Errorf("type name %s not supported", t.GetTypeName()) diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go index 38abff9d8..29fd55034 100644 --- a/internal/message/accountprocess.go +++ b/internal/message/accountprocess.go @@ -309,6 +309,57 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri return accounts, nil } +func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) { + blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID) + if err != nil { + return nil, NewErrorInternalError(err) + } + + if blocked { + return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts")) + } + + following := []gtsmodel.Follow{} + accounts := []apimodel.Account{} + if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return accounts, nil + } + return nil, NewErrorInternalError(err) + } + + for _, f := range following { + blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID) + if err != nil { + return nil, NewErrorInternalError(err) + } + if blocked { + continue + } + + a := >smodel.Account{} + if err := p.db.GetByID(f.TargetAccountID, a); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + continue + } + return nil, NewErrorInternalError(err) + } + + // derefence account fields in case we haven't done it already + if err := p.dereferenceAccountFields(a, authed.Account.Username); 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) + } + + account, err := p.tc.AccountToMastoPublic(a) + if err != nil { + return nil, NewErrorInternalError(err) + } + accounts = append(accounts, *account) + } + return accounts, nil +} + func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) { if authed == nil || authed.Account == nil { return nil, NewErrorForbidden(errors.New("not authed")) @@ -410,3 +461,85 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou // return whatever relationship results from this return p.AccountRelationshipGet(authed, form.TargetAccountID) } + +func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) { + // if there's a block between the accounts we shouldn't do anything + blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID) + if err != nil { + return nil, NewErrorInternalError(err) + } + if blocked { + return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts")) + } + + // make sure the target account actually exists in our db + targetAcct := >smodel.Account{} + if err := p.db.GetByID(targetAccountID, targetAcct); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) + } + } + + // check if a follow request exists, and remove it if it does (storing the URI for later) + var frChanged bool + var frURI string + fr := >smodel.FollowRequest{} + if err := p.db.GetWhere([]db.Where{ + {Key: "account_id", Value: authed.Account.ID}, + {Key: "target_account_id", Value: targetAccountID}, + }, fr); err == nil { + frURI = fr.URI + if err := p.db.DeleteByID(fr.ID, fr); err != nil { + return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err)) + } + frChanged = true + } + + // now do the same thing for any existing follow + var fChanged bool + var fURI string + f := >smodel.Follow{} + if err := p.db.GetWhere([]db.Where{ + {Key: "account_id", Value: authed.Account.ID}, + {Key: "target_account_id", Value: targetAccountID}, + }, f); err == nil { + fURI = f.URI + if err := p.db.DeleteByID(f.ID, f); err != nil { + return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err)) + } + fChanged = true + } + + // follow request status changed so send the UNDO activity to the channel for async processing + if frChanged { + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsFollow, + APActivityType: gtsmodel.ActivityStreamsUndo, + GTSModel: >smodel.Follow{ + AccountID: authed.Account.ID, + TargetAccountID: targetAccountID, + URI: frURI, + }, + OriginAccount: authed.Account, + TargetAccount: targetAcct, + } + } + + // follow status changed so send the UNDO activity to the channel for async processing + if fChanged { + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsFollow, + APActivityType: gtsmodel.ActivityStreamsUndo, + GTSModel: >smodel.Follow{ + AccountID: authed.Account.ID, + TargetAccountID: targetAccountID, + URI: fURI, + }, + OriginAccount: authed.Account, + TargetAccount: targetAcct, + } + } + + // return whatever relationship results from all this + return p.AccountRelationshipGet(authed, targetAccountID) +} diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go index 3c7c30e27..e782fef2c 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -46,7 +46,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht // we might already have an entry for this account so check that first requestingAccount := >smodel.Account{} - err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount) + err = p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount) if err == nil { // we do have it yay, return it return requestingAccount, nil diff --git a/internal/message/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go index 3602fbcc0..27e533c7f 100644 --- a/internal/message/fromclientapiprocess.go +++ b/internal/message/fromclientapiprocess.go @@ -71,6 +71,17 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return errors.New("accept was not parseable as *gtsmodel.Follow") } return p.federateAcceptFollowRequest(follow) + case gtsmodel.ActivityStreamsUndo: + // UNDO + switch clientMsg.APObjectType { + // UNDO FOLLOW + case gtsmodel.ActivityStreamsFollow: + follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) + if !ok { + return errors.New("undo was not parseable as *gtsmodel.Follow") + } + return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) + } } return nil } @@ -117,11 +128,46 @@ func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmo return err } +func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { + // recreate the follow + asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount) + if err != nil { + return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err) + } + + targetAccountURI, err := url.Parse(targetAccount.URI) + if err != nil { + return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err) + } + + // create an Undo and set the appropriate actor on it + undo := streams.NewActivityStreamsUndo() + undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor()) + + // Set the recreated follow as the 'object' property. + undoObject := streams.NewActivityStreamsObjectProperty() + undoObject.AppendActivityStreamsFollow(asFollow) + undo.SetActivityStreamsObject(undoObject) + + // Set the To of the undo as the target of the recreated follow + undoTo := streams.NewActivityStreamsToProperty() + undoTo.AppendIRI(targetAccountURI) + undo.SetActivityStreamsTo(undoTo) + + outboxIRI, err := url.Parse(originAccount.OutboxURI) + if err != nil { + return fmt.Errorf("federateUnfollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err) + } + + // send off the Undo + _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo) + return err +} + func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow) error { // TODO: tidy up this whole function -- move most of the logic for the conversion to the type converter because this is just a mess! Shame on me! - followAccepter := >smodel.Account{} if err := p.db.GetByID(follow.TargetAccountID, followAccepter); err != nil { return fmt.Errorf("error federating follow accept: %s", err) diff --git a/internal/message/fromfederatorprocess.go b/internal/message/fromfederatorprocess.go index 96f9ab0e5..ffaa1b93b 100644 --- a/internal/message/fromfederatorprocess.go +++ b/internal/message/fromfederatorprocess.go @@ -157,7 +157,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { // it might have been processed elsewhere so check first if it's already in the database or not maybeAttachment := >smodel.MediaAttachment{} - err := p.db.GetWhere("remote_url", a.RemoteURL, maybeAttachment) + err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment) if err == nil { // we already have it in the db, dereferenced, no need to do it again l.Debugf("attachment already exists with id %s", maybeAttachment.ID) @@ -206,7 +206,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error { m.OriginAccountURI = status.GTSAccount.URI targetAccount := >smodel.Account{} - if err := p.db.GetWhere("uri", uri.String(), targetAccount); err != nil { + if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil { // proper error if _, ok := err.(db.ErrNoEntries); !ok { return fmt.Errorf("db error checking for account with uri %s", uri.String()) diff --git a/internal/message/instanceprocess.go b/internal/message/instanceprocess.go index 05ea103fd..f544fbf30 100644 --- a/internal/message/instanceprocess.go +++ b/internal/message/instanceprocess.go @@ -22,12 +22,13 @@ import ( "fmt" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) { i := >smodel.Instance{} - if err := p.db.GetWhere("domain", domain, i); err != nil { + if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil { return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err)) } diff --git a/internal/message/processor.go b/internal/message/processor.go index 3d1995a26..2b41aa7b3 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -73,10 +73,14 @@ type Processor interface { AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) // AccountFollowersGet fetches a list of the target account's followers. AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) + // AccountFollowingGet fetches a list of the accounts that target account is following. + AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) // AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account. AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) // AccountFollowCreate handles a follow request to an account, either remote or local. AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) + // AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local. + AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) @@ -204,15 +208,11 @@ func (p *processor) Start() error { DistLoop: for { select { - // case clientMsg := <-p.toClientAPI: - // p.log.Infof("received message TO client API: %+v", clientMsg) case clientMsg := <-p.fromClientAPI: p.log.Infof("received message FROM client API: %+v", clientMsg) if err := p.processFromClientAPI(clientMsg); err != nil { p.log.Error(err) } - // case federatorMsg := <-p.toFederator: - // p.log.Infof("received message TO federator: %+v", federatorMsg) case federatorMsg := <-p.fromFederator: p.log.Infof("received message FROM federator: %+v", federatorMsg) if err := p.processFromFederator(federatorMsg); err != nil { diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go index 195db838f..04319ee0b 100644 --- a/internal/oauth/tokenstore.go +++ b/internal/oauth/tokenstore.go @@ -106,17 +106,17 @@ func (pts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error // RemoveByCode deletes a token from the DB based on the Code field func (pts *tokenStore) RemoveByCode(ctx context.Context, code string) error { - return pts.db.DeleteWhere("code", code, &Token{}) + return pts.db.DeleteWhere([]db.Where{{Key: "code", Value: code}}, &Token{}) } // RemoveByAccess deletes a token from the DB based on the Access field func (pts *tokenStore) RemoveByAccess(ctx context.Context, access string) error { - return pts.db.DeleteWhere("access", access, &Token{}) + return pts.db.DeleteWhere([]db.Where{{Key: "access", Value: access}}, &Token{}) } // RemoveByRefresh deletes a token from the DB based on the Refresh field func (pts *tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { - return pts.db.DeleteWhere("refresh", refresh, &Token{}) + return pts.db.DeleteWhere([]db.Where{{Key: "refresh", Value: refresh}}, &Token{}) } // GetByCode selects a token from the DB based on the Code field @@ -127,7 +127,7 @@ func (pts *tokenStore) GetByCode(ctx context.Context, code string) (oauth2.Token pgt := &Token{ Code: code, } - if err := pts.db.GetWhere("code", code, pgt); err != nil { + if err := pts.db.GetWhere([]db.Where{{Key: "code", Value: code}}, pgt); err != nil { return nil, err } return TokenToOauthToken(pgt), nil @@ -141,7 +141,7 @@ func (pts *tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.T pgt := &Token{ Access: access, } - if err := pts.db.GetWhere("access", access, pgt); err != nil { + if err := pts.db.GetWhere([]db.Where{{Key: "access", Value: access}}, pgt); err != nil { return nil, err } return TokenToOauthToken(pgt), nil @@ -155,7 +155,7 @@ func (pts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2 pgt := &Token{ Refresh: refresh, } - if err := pts.db.GetWhere("refresh", refresh, pgt); err != nil { + if err := pts.db.GetWhere([]db.Where{{Key: "refresh", Value: refresh}}, pgt); err != nil { return nil, err } return TokenToOauthToken(pgt), nil diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index d6541719d..bf1ef7f45 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -37,7 +37,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode uri := uriProp.GetIRI() acct := >smodel.Account{} - err := c.db.GetWhere("uri", uri.String(), acct) + 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 @@ -220,7 +220,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e status.APStatusOwnerURI = attributedTo.String() statusOwner := >smodel.Account{} - if err := c.db.GetWhere("uri", attributedTo.String(), statusOwner); err != nil { + if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String()}}, statusOwner); err != nil { return nil, fmt.Errorf("couldn't get status owner from db: %s", err) } status.AccountID = statusOwner.ID @@ -235,7 +235,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e // now we can check if we have the replied-to status in our db already inReplyToStatus := >smodel.Status{} - if err := c.db.GetWhere("uri", inReplyToURI.String(), inReplyToStatus); err == nil { + if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: inReplyToURI.String()}}, inReplyToStatus); err == nil { // we have the status in our database already // so we can set these fields here and then... status.InReplyToID = inReplyToStatus.ID @@ -322,7 +322,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo return nil, errors.New("error extracting actor property from follow") } originAccount := >smodel.Account{} - if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil { + if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -331,7 +331,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo return nil, errors.New("error extracting object property from follow") } targetAccount := >smodel.Account{} - if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil { + if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -356,7 +356,7 @@ func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, e return nil, errors.New("error extracting actor property from follow") } originAccount := >smodel.Account{} - if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil { + if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } @@ -365,7 +365,7 @@ func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, e return nil, errors.New("error extracting object property from follow") } targetAccount := >smodel.Account{} - if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil { + if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil { return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) }