commit 9ce4234b9fd1e201faf015df52bfc35db259dd46
parent 365c3bf5d7d06b015f5606bed7f7847fb15751fe
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Fri, 1 Oct 2021 19:08:50 +0200
Follow request auto approval (#259)
* start messing about
* fiddle more
* Tests & fiddling
Diffstat:
5 files changed, 242 insertions(+), 15 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -20,12 +20,14 @@ package processing_test
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
@@ -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)