mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-10 15:04:17 +00:00
Follow request auto approval (#259)
* start messing about * fiddle more * Tests & fiddling
This commit is contained in:
parent
365c3bf5d7
commit
9ce4234b9f
5 changed files with 242 additions and 15 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue