mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 19:52:25 -05: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") | 				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…
	
	Add table
		Add a link
		
	
		Reference in a new issue