From 9ce4234b9fd1e201faf015df52bfc35db259dd46 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Fri, 1 Oct 2021 19:08:50 +0200 Subject: [PATCH] Follow request auto approval (#259) * start messing about * fiddle more * Tests & fiddling --- internal/processing/fromclientapi.go | 2 +- internal/processing/fromcommon.go | 17 ++- internal/processing/fromfederator.go | 44 ++++++-- internal/processing/fromfederator_test.go | 129 ++++++++++++++++++++++ internal/processing/processor_test.go | 65 ++++++++++- 5 files changed, 242 insertions(+), 15 deletions(-) diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 79860b5a9..e15299d70 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -61,7 +61,7 @@ func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest") } - if err := p.notifyFollowRequest(ctx, followRequest, clientMsg.TargetAccount); err != nil { + if err := p.notifyFollowRequest(ctx, followRequest); err != nil { return err } diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index 88d7405a8..e14c36fd4 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -109,9 +109,20 @@ func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) e return nil } -func (p *processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest, receivingAccount *gtsmodel.Account) error { +func (p *processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { + // make sure we have the target account pinned on the follow request + if followRequest.TargetAccount == nil { + a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID) + if err != nil { + return err + } + followRequest.TargetAccount = a + } + targetAccount := followRequest.TargetAccount + // return if this isn't a local account - if receivingAccount.Domain != "" { + if targetAccount.Domain != "" { + // this isn't a local account so we've got nothing to do here return nil } @@ -137,7 +148,7 @@ func (p *processor) notifyFollowRequest(ctx context.Context, followRequest *gtsm return fmt.Errorf("notifyStatus: error converting notification to masto representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, receivingAccount); err != nil { + if err := p.streamingProcessor.StreamNotificationToAccount(mastoNotif, targetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index ffaf625d3..1ef29264e 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -77,14 +77,45 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa } case ap.ActivityFollow: // CREATE A FOLLOW REQUEST - incomingFollowRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) + followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) if !ok { return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest") } - if err := p.notifyFollowRequest(ctx, incomingFollowRequest, federatorMsg.ReceivingAccount); err != nil { + if followRequest.TargetAccount == nil { + a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID) + if err != nil { + return err + } + followRequest.TargetAccount = a + } + targetAccount := followRequest.TargetAccount + + if targetAccount.Locked { + // if the account is locked just notify the follow request and nothing else + return p.notifyFollowRequest(ctx, followRequest) + } + + if followRequest.Account == nil { + a, err := p.db.GetAccountByID(ctx, followRequest.AccountID) + if err != nil { + return err + } + followRequest.Account = a + } + originAccount := followRequest.Account + + // if the target account isn't locked, we should already accept the follow and notify about the new follower instead + follow, err := p.db.AcceptFollowRequest(ctx, followRequest.AccountID, followRequest.TargetAccountID) + if err != nil { return err } + + if err := p.federateAcceptFollowRequest(ctx, follow, originAccount, targetAccount); err != nil { + return err + } + + return p.notifyFollow(ctx, follow, targetAccount) case ap.ActivityAnnounce: // CREATE AN ANNOUNCE incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status) @@ -194,14 +225,7 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa switch federatorMsg.APObjectType { case ap.ActivityFollow: // ACCEPT A FOLLOW - follow, ok := federatorMsg.GTSModel.(*gtsmodel.Follow) - if !ok { - return errors.New("follow was not parseable as *gtsmodel.Follow") - } - - if err := p.notifyFollow(ctx, follow, federatorMsg.ReceivingAccount); err != nil { - return err - } + // nothing to do here } } diff --git a/internal/processing/fromfederator_test.go b/internal/processing/fromfederator_test.go index 605a18bdc..4f100d4cb 100644 --- a/internal/processing/fromfederator_test.go +++ b/internal/processing/fromfederator_test.go @@ -20,12 +20,14 @@ import ( "context" + "encoding/json" "fmt" "testing" "time" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" @@ -357,6 +359,133 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() { suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) } +func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() { + ctx := context.Background() + + originAccount := suite.testAccounts["remote_account_1"] + + // target is a locked account + targetAccount := suite.testAccounts["local_account_2"] + + stream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), targetAccount, "user") + suite.NoError(errWithCode) + + // put the follow request in the database as though it had passed through the federating db already + satanFollowRequestTurtle := >smodel.FollowRequest{ + ID: "01FGRYAVAWWPP926J175QGM0WV", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AccountID: originAccount.ID, + Account: originAccount, + TargetAccountID: targetAccount.ID, + TargetAccount: targetAccount, + ShowReblogs: true, + URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI), + Notify: false, + } + + err := suite.db.Put(ctx, satanFollowRequestTurtle) + suite.NoError(err) + + err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityCreate, + GTSModel: satanFollowRequestTurtle, + ReceivingAccount: targetAccount, + }) + suite.NoError(err) + + // a notification should be streamed + msg := <-stream.Messages + suite.Equal("notification", msg.Event) + suite.NotEmpty(msg.Payload) + suite.EqualValues([]string{"user"}, msg.Stream) + notif := &model.Notification{} + err = json.Unmarshal([]byte(msg.Payload), notif) + suite.NoError(err) + suite.Equal("follow_request", notif.Type) + suite.Equal(originAccount.ID, notif.Account.ID) + + // no messages should have been sent out, since we didn't need to federate an accept + suite.Empty(suite.sentHTTPRequests) +} + +func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() { + ctx := context.Background() + + originAccount := suite.testAccounts["remote_account_1"] + + // target is an unlocked account + targetAccount := suite.testAccounts["local_account_1"] + + stream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), targetAccount, "user") + suite.NoError(errWithCode) + + // put the follow request in the database as though it had passed through the federating db already + satanFollowRequestTurtle := >smodel.FollowRequest{ + ID: "01FGRYAVAWWPP926J175QGM0WV", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AccountID: originAccount.ID, + Account: originAccount, + TargetAccountID: targetAccount.ID, + TargetAccount: targetAccount, + ShowReblogs: true, + URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI), + Notify: false, + } + + err := suite.db.Put(ctx, satanFollowRequestTurtle) + suite.NoError(err) + + err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityCreate, + GTSModel: satanFollowRequestTurtle, + ReceivingAccount: targetAccount, + }) + suite.NoError(err) + + // a notification should be streamed + msg := <-stream.Messages + suite.Equal("notification", msg.Event) + suite.NotEmpty(msg.Payload) + suite.EqualValues([]string{"user"}, msg.Stream) + notif := &model.Notification{} + err = json.Unmarshal([]byte(msg.Payload), notif) + suite.NoError(err) + suite.Equal("follow", notif.Type) + suite.Equal(originAccount.ID, notif.Account.ID) + + // an accept message should be sent to satan's inbox + suite.Len(suite.sentHTTPRequests, 1) + acceptBytes := suite.sentHTTPRequests[originAccount.InboxURI] + accept := &struct { + Actor string `json:"actor"` + ID string `json:"id"` + Object struct { + Actor string `json:"actor"` + ID string `json:"id"` + Object string `json:"object"` + To string `json:"to"` + Type string `json:"type"` + } + To string `json:"to"` + Type string `json:"type"` + }{} + err = json.Unmarshal(acceptBytes, accept) + suite.NoError(err) + + suite.Equal(targetAccount.URI, accept.Actor) + suite.Equal(originAccount.URI, accept.Object.Actor) + suite.Equal(satanFollowRequestTurtle.URI, accept.Object.ID) + suite.Equal(targetAccount.URI, accept.Object.Object) + suite.Equal(targetAccount.URI, accept.Object.To) + suite.Equal("Follow", accept.Object.Type) + suite.Equal(originAccount.URI, accept.To) + suite.Equal("Accept", accept.Type) +} + func TestFromFederatorTestSuite(t *testing.T) { suite.Run(t, &FromFederatorTestSuite{}) } diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index e0f44b0d7..1c4dfb32f 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -19,9 +19,15 @@ package processing_test import ( + "bytes" "context" + "encoding/json" + "io" + "io/ioutil" + "net/http" "git.iim.gay/grufwub/go-store/kv" + "github.com/go-fed/activity/streams" "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" @@ -64,6 +70,8 @@ type ProcessingStandardTestSuite struct { testAutheds map[string]*oauth.Auth testBlocks map[string]*gtsmodel.Block + sentHTTPRequests map[string][]byte + processor processing.Processor } @@ -93,7 +101,62 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { suite.log = testrig.NewTestLog() suite.storage = testrig.NewTestStorage() suite.typeconverter = testrig.NewTestTypeConverter(suite.db) - suite.transportController = testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db) + + // make an http client that stores POST requests it receives into a map, + // and also responds to correctly to dereference requests + suite.sentHTTPRequests = make(map[string][]byte) + httpClient := testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) { + if req.Method == http.MethodPost && req.Body != nil { + requestBytes, err := ioutil.ReadAll(req.Body) + if err != nil { + panic(err) + } + if err := req.Body.Close(); err != nil { + panic(err) + } + suite.sentHTTPRequests[req.URL.String()] = requestBytes + } + + if req.URL.String() == suite.testAccounts["remote_account_1"].URI { + // the request is for remote account 1 + satan := suite.testAccounts["remote_account_1"] + + satanAS, err := suite.typeconverter.AccountToAS(context.Background(), satan) + if err != nil { + panic(err) + } + + satanI, err := streams.Serialize(satanAS) + if err != nil { + panic(err) + } + satanJson, err := json.Marshal(satanI) + if err != nil { + panic(err) + } + responseType := "application/activity+json" + + reader := bytes.NewReader(satanJson) + readCloser := io.NopCloser(reader) + response := &http.Response{ + StatusCode: 200, + Body: readCloser, + ContentLength: int64(len(satanJson)), + Header: http.Header{ + "content-type": {responseType}, + }, + } + return response, nil + } + + r := ioutil.NopCloser(bytes.NewReader([]byte{})) + return &http.Response{ + StatusCode: 200, + Body: r, + }, nil + }) + + suite.transportController = testrig.NewTestTransportController(httpClient, suite.db) suite.federator = testrig.NewTestFederator(suite.db, suite.transportController, suite.storage) suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)