Follow request auto approval (#259)
* start messing about * fiddle more * Tests & fiddling
This commit is contained in:
parent
365c3bf5d7
commit
9ce4234b9f
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
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:
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,14 @@ package processing_test
|
||||||
|
|
||||||
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 := >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) {
|
func TestFromFederatorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &FromFederatorTestSuite{})
|
suite.Run(t, &FromFederatorTestSuite{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue