Follow request auto approval (#259)

* start messing about

* fiddle more

* Tests & fiddling
This commit is contained in:
tobi 2021-10-01 19:08:50 +02:00 committed by GitHub
parent 365c3bf5d7
commit 9ce4234b9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 242 additions and 15 deletions

View File

@ -61,7 +61,7 @@ func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages
return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest") 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 return err
} }

View File

@ -109,9 +109,20 @@ func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) e
return nil 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 // 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 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) 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) return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err)
} }

View File

@ -77,14 +77,45 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa
} }
case ap.ActivityFollow: case ap.ActivityFollow:
// CREATE A FOLLOW REQUEST // CREATE A FOLLOW REQUEST
incomingFollowRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest)
if !ok { if !ok {
return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest") 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 return err
} }
if err := p.federateAcceptFollowRequest(ctx, follow, originAccount, targetAccount); err != nil {
return err
}
return p.notifyFollow(ctx, follow, targetAccount)
case ap.ActivityAnnounce: case ap.ActivityAnnounce:
// CREATE AN ANNOUNCE // CREATE AN ANNOUNCE
incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status) incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
@ -194,14 +225,7 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa
switch federatorMsg.APObjectType { switch federatorMsg.APObjectType {
case ap.ActivityFollow: case ap.ActivityFollow:
// ACCEPT A FOLLOW // ACCEPT A FOLLOW
follow, ok := federatorMsg.GTSModel.(*gtsmodel.Follow) // nothing to do here
if !ok {
return errors.New("follow was not parseable as *gtsmodel.Follow")
}
if err := p.notifyFollow(ctx, follow, federatorMsg.ReceivingAccount); err != nil {
return err
}
} }
} }

View File

@ -20,12 +20,14 @@
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
@ -357,6 +359,133 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) 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 := &gtsmodel.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 := &gtsmodel.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) { func TestFromFederatorTestSuite(t *testing.T) {
suite.Run(t, &FromFederatorTestSuite{}) suite.Run(t, &FromFederatorTestSuite{})
} }

View File

@ -19,9 +19,15 @@
package processing_test package processing_test
import ( import (
"bytes"
"context" "context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"git.iim.gay/grufwub/go-store/kv" "git.iim.gay/grufwub/go-store/kv"
"github.com/go-fed/activity/streams"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
@ -64,6 +70,8 @@ type ProcessingStandardTestSuite struct {
testAutheds map[string]*oauth.Auth testAutheds map[string]*oauth.Auth
testBlocks map[string]*gtsmodel.Block testBlocks map[string]*gtsmodel.Block
sentHTTPRequests map[string][]byte
processor processing.Processor processor processing.Processor
} }
@ -93,7 +101,62 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
suite.log = testrig.NewTestLog() suite.log = testrig.NewTestLog()
suite.storage = testrig.NewTestStorage() suite.storage = testrig.NewTestStorage()
suite.typeconverter = testrig.NewTestTypeConverter(suite.db) 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.federator = testrig.NewTestFederator(suite.db, suite.transportController, suite.storage)
suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)