mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 21:12:24 -05:00 
			
		
		
		
	Follow request improvements (#282)
* tiny doc update * add rejectfollowrequest to db * add follow request reject to processor * add reject handler * tidy up follow request api * tidy up federation call * regenerate swagger docs * api endpoint tests * processor test * add reject federatingdb handler * start writing reject tests * test reject follow request * go fmt * increase sleep for slow test setups * more relaxed time.sleep
This commit is contained in:
		
					parent
					
						
							
								107685e22e
							
						
					
				
			
			
				commit
				
					
						15621f5324
					
				
			
		
					 24 changed files with 1256 additions and 69 deletions
				
			
		|  | @ -215,7 +215,9 @@ You can install go-swagger following the instructions [here](https://goswagger.i | ||||||
| 
 | 
 | ||||||
| If you change Swagger annotations on any of the API paths, you can generate a new Swagger file at `./docs/api/swagger.yaml` by running: | If you change Swagger annotations on any of the API paths, you can generate a new Swagger file at `./docs/api/swagger.yaml` by running: | ||||||
| 
 | 
 | ||||||
| `swagger generate spec -o docs/api/swagger.yaml --scan-models` | ```bash | ||||||
|  | swagger generate spec -o docs/api/swagger.yaml --scan-models | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
| ## CI/CD configuration | ## CI/CD configuration | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2450,6 +2450,115 @@ paths: | ||||||
|       summary: Get an array of accounts that requesting account has blocked. |       summary: Get an array of accounts that requesting account has blocked. | ||||||
|       tags: |       tags: | ||||||
|       - blocks |       - blocks | ||||||
|  |   /api/v1/follow_requests: | ||||||
|  |     get: | ||||||
|  |       description: |- | ||||||
|  |         The next and previous queries can be parsed from the returned Link header. | ||||||
|  |         Example: | ||||||
|  | 
 | ||||||
|  |         ``` | ||||||
|  |         <https://example.org/api/v1/follow_requests?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/follow_requests?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev" | ||||||
|  |         ```` | ||||||
|  |       operationId: getFollowRequests | ||||||
|  |       parameters: | ||||||
|  |       - default: 40 | ||||||
|  |         description: Number of accounts to return. | ||||||
|  |         in: query | ||||||
|  |         name: limit | ||||||
|  |         type: integer | ||||||
|  |       produces: | ||||||
|  |       - application/json | ||||||
|  |       responses: | ||||||
|  |         "200": | ||||||
|  |           description: "" | ||||||
|  |           headers: | ||||||
|  |             Link: | ||||||
|  |               description: Links to the next and previous queries. | ||||||
|  |               type: string | ||||||
|  |           schema: | ||||||
|  |             items: | ||||||
|  |               $ref: '#/definitions/account' | ||||||
|  |             type: array | ||||||
|  |         "400": | ||||||
|  |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|  |         "403": | ||||||
|  |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |       security: | ||||||
|  |       - OAuth2 Bearer: | ||||||
|  |         - read:follows | ||||||
|  |       summary: Get an array of accounts that have requested to follow you. | ||||||
|  |       tags: | ||||||
|  |       - follow_requests | ||||||
|  |   /api/v1/follow_requests/{account_id}/authorize: | ||||||
|  |     post: | ||||||
|  |       description: Accept a follow request and put the requesting account in your | ||||||
|  |         'followers' list. | ||||||
|  |       operationId: authorizeFollowRequest | ||||||
|  |       parameters: | ||||||
|  |       - description: ID of the account requesting to follow you. | ||||||
|  |         in: path | ||||||
|  |         name: account_id | ||||||
|  |         required: true | ||||||
|  |         type: string | ||||||
|  |       produces: | ||||||
|  |       - application/json | ||||||
|  |       responses: | ||||||
|  |         "200": | ||||||
|  |           description: Your relationship to this account. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/accountRelationship' | ||||||
|  |         "400": | ||||||
|  |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|  |         "403": | ||||||
|  |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|  |       security: | ||||||
|  |       - OAuth2 Bearer: | ||||||
|  |         - write:follows | ||||||
|  |       summary: Accept/authorize follow request from the given account ID. | ||||||
|  |       tags: | ||||||
|  |       - follow_requests | ||||||
|  |   /api/v1/follow_requests/{account_id}/reject: | ||||||
|  |     post: | ||||||
|  |       operationId: rejectFollowRequest | ||||||
|  |       parameters: | ||||||
|  |       - description: ID of the account requesting to follow you. | ||||||
|  |         in: path | ||||||
|  |         name: account_id | ||||||
|  |         required: true | ||||||
|  |         type: string | ||||||
|  |       produces: | ||||||
|  |       - application/json | ||||||
|  |       responses: | ||||||
|  |         "200": | ||||||
|  |           description: Your relationship to this account. | ||||||
|  |           schema: | ||||||
|  |             $ref: '#/definitions/accountRelationship' | ||||||
|  |         "400": | ||||||
|  |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|  |         "403": | ||||||
|  |           description: forbidden | ||||||
|  |         "404": | ||||||
|  |           description: not found | ||||||
|  |         "500": | ||||||
|  |           description: internal server error | ||||||
|  |       security: | ||||||
|  |       - OAuth2 Bearer: | ||||||
|  |         - write:follows | ||||||
|  |       summary: Reject/deny follow request from the given account ID. | ||||||
|  |       tags: | ||||||
|  |       - follow_requests | ||||||
|   /api/v1/instance: |   /api/v1/instance: | ||||||
|     get: |     get: | ||||||
|       description: |- |       description: |- | ||||||
|  |  | ||||||
							
								
								
									
										99
									
								
								internal/api/client/followrequest/authorize.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								internal/api/client/followrequest/authorize.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package followrequest | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // FollowRequestAuthorizePOSTHandler swagger:operation POST /api/v1/follow_requests/{account_id}/authorize authorizeFollowRequest | ||||||
|  | // | ||||||
|  | // Accept/authorize follow request from the given account ID. | ||||||
|  | // | ||||||
|  | // Accept a follow request and put the requesting account in your 'followers' list. | ||||||
|  | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - follow_requests | ||||||
|  | // | ||||||
|  | // produces: | ||||||
|  | // - application/json | ||||||
|  | // | ||||||
|  | // parameters: | ||||||
|  | // - name: account_id | ||||||
|  | //   type: string | ||||||
|  | //   description: ID of the account requesting to follow you. | ||||||
|  | //   in: path | ||||||
|  | //   required: true | ||||||
|  | // | ||||||
|  | // security: | ||||||
|  | // - OAuth2 Bearer: | ||||||
|  | //   - write:follows | ||||||
|  | // | ||||||
|  | // responses: | ||||||
|  | //   '200': | ||||||
|  | //     name: account relationship | ||||||
|  | //     description: Your relationship to this account. | ||||||
|  | //     schema: | ||||||
|  | //       "$ref": "#/definitions/accountRelationship" | ||||||
|  | //   '400': | ||||||
|  | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
|  | func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) { | ||||||
|  | 	l := logrus.WithField("func", "FollowRequestAuthorizePOSTHandler") | ||||||
|  | 
 | ||||||
|  | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		l.Debugf("couldn't auth: %s", err) | ||||||
|  | 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { | ||||||
|  | 		l.Debugf("couldn't auth: %s", err) | ||||||
|  | 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	originAccountID := c.Param(IDKey) | ||||||
|  | 	if originAccountID == "" { | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		l.Debug(errWithCode.Error()) | ||||||
|  | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusOK, relationship) | ||||||
|  | } | ||||||
							
								
								
									
										87
									
								
								internal/api/client/followrequest/authorize_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								internal/api/client/followrequest/authorize_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package followrequest_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type AuthorizeTestSuite struct { | ||||||
|  | 	FollowRequestStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *AuthorizeTestSuite) TestAuthorize() { | ||||||
|  | 	requestingAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 	targetAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	// put a follow request in the database | ||||||
|  | 	fr := >smodel.FollowRequest{ | ||||||
|  | 		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", | ||||||
|  | 		CreatedAt:       time.Now(), | ||||||
|  | 		UpdatedAt:       time.Now(), | ||||||
|  | 		URI:             fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", requestingAccount.URI), | ||||||
|  | 		AccountID:       requestingAccount.ID, | ||||||
|  | 		TargetAccountID: targetAccount.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := suite.db.Put(context.Background(), fr) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", requestingAccount.ID), "") | ||||||
|  | 
 | ||||||
|  | 	ctx.Params = gin.Params{ | ||||||
|  | 		gin.Param{ | ||||||
|  | 			Key:   followrequest.IDKey, | ||||||
|  | 			Value: requestingAccount.ID, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.followRequestModule.FollowRequestAuthorizePOSTHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 1. we should have OK because our request was valid | ||||||
|  | 	suite.Equal(http.StatusOK, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	// 2. we should have no error message in the result body | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	// check the response | ||||||
|  | 	b, err := ioutil.ReadAll(result.Body) | ||||||
|  | 	assert.NoError(suite.T(), err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestAuthorizeTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, &AuthorizeTestSuite{}) | ||||||
|  | } | ||||||
|  | @ -1,27 +0,0 @@ | ||||||
| /* |  | ||||||
|    GoToSocial |  | ||||||
|    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org |  | ||||||
| 
 |  | ||||||
|    This program is free software: you can redistribute it and/or modify |  | ||||||
|    it under the terms of the GNU Affero General Public License as published by |  | ||||||
|    the Free Software Foundation, either version 3 of the License, or |  | ||||||
|    (at your option) any later version. |  | ||||||
| 
 |  | ||||||
|    This program is distributed in the hope that it will be useful, |  | ||||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|    GNU Affero General Public License for more details. |  | ||||||
| 
 |  | ||||||
|    You should have received a copy of the GNU Affero General Public License |  | ||||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| package followrequest |  | ||||||
| 
 |  | ||||||
| import "github.com/gin-gonic/gin" |  | ||||||
| 
 |  | ||||||
| // FollowRequestDenyPOSTHandler deals with follow request rejection. It should be served at |  | ||||||
| // /api/v1/follow_requests/:id/reject |  | ||||||
| func (m *Module) FollowRequestDenyPOSTHandler(c *gin.Context) { |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -28,21 +28,20 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	// IDKey is for status UUIDs | 	// IDKey is for account IDs | ||||||
| 	IDKey = "id" | 	IDKey = "id" | ||||||
| 	// BasePath is the base path for serving the follow request API | 	// BasePath is the base path for serving the follow request API | ||||||
| 	BasePath = "/api/v1/follow_requests" | 	BasePath = "/api/v1/follow_requests" | ||||||
| 	// BasePathWithID is just the base path with the ID key in it. | 	// BasePathWithID is just the base path with the ID key in it. | ||||||
| 	// Use this anywhere you need to know the ID of the follow request being queried. | 	// Use this anywhere you need to know the ID of the account that owns the follow request being queried. | ||||||
| 	BasePathWithID = BasePath + "/:" + IDKey | 	BasePathWithID = BasePath + "/:" + IDKey | ||||||
| 
 | 	// AuthorizePath is used for authorizing follow requests | ||||||
| 	// AcceptPath is used for accepting follow requests | 	AuthorizePath = BasePathWithID + "/authorize" | ||||||
| 	AcceptPath = BasePathWithID + "/authorize" | 	// RejectPath is used for rejecting follow requests | ||||||
| 	// DenyPath is used for denying follow requests | 	RejectPath = BasePathWithID + "/reject" | ||||||
| 	DenyPath = BasePathWithID + "/reject" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Module implements the ClientAPIModule interface for every related to interacting with follow requests | // Module implements the ClientAPIModule interface | ||||||
| type Module struct { | type Module struct { | ||||||
| 	config    *config.Config | 	config    *config.Config | ||||||
| 	processor processing.Processor | 	processor processing.Processor | ||||||
|  | @ -59,7 +58,7 @@ func New(config *config.Config, processor processing.Processor) api.ClientModule | ||||||
| // Route attaches all routes from this module to the given router | // Route attaches all routes from this module to the given router | ||||||
| func (m *Module) Route(r router.Router) error { | func (m *Module) Route(r router.Router) error { | ||||||
| 	r.AttachHandler(http.MethodGet, BasePath, m.FollowRequestGETHandler) | 	r.AttachHandler(http.MethodGet, BasePath, m.FollowRequestGETHandler) | ||||||
| 	r.AttachHandler(http.MethodPost, AcceptPath, m.FollowRequestAcceptPOSTHandler) | 	r.AttachHandler(http.MethodPost, AuthorizePath, m.FollowRequestAuthorizePOSTHandler) | ||||||
| 	r.AttachHandler(http.MethodPost, DenyPath, m.FollowRequestDenyPOSTHandler) | 	r.AttachHandler(http.MethodPost, RejectPath, m.FollowRequestRejectPOSTHandler) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										105
									
								
								internal/api/client/followrequest/followrequest_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								internal/api/client/followrequest/followrequest_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package followrequest_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 
 | ||||||
|  | 	"git.iim.gay/grufwub/go-store/kv" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/federation" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/processing" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type FollowRequestStandardTestSuite struct { | ||||||
|  | 	suite.Suite | ||||||
|  | 	config    *config.Config | ||||||
|  | 	db        db.DB | ||||||
|  | 	storage   *kv.KVStore | ||||||
|  | 	federator federation.Federator | ||||||
|  | 	processor processing.Processor | ||||||
|  | 
 | ||||||
|  | 	// standard suite models | ||||||
|  | 	testTokens       map[string]*gtsmodel.Token | ||||||
|  | 	testClients      map[string]*gtsmodel.Client | ||||||
|  | 	testApplications map[string]*gtsmodel.Application | ||||||
|  | 	testUsers        map[string]*gtsmodel.User | ||||||
|  | 	testAccounts     map[string]*gtsmodel.Account | ||||||
|  | 	testAttachments  map[string]*gtsmodel.MediaAttachment | ||||||
|  | 	testStatuses     map[string]*gtsmodel.Status | ||||||
|  | 
 | ||||||
|  | 	// module being tested | ||||||
|  | 	followRequestModule *followrequest.Module | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *FollowRequestStandardTestSuite) SetupSuite() { | ||||||
|  | 	suite.testTokens = testrig.NewTestTokens() | ||||||
|  | 	suite.testClients = testrig.NewTestClients() | ||||||
|  | 	suite.testApplications = testrig.NewTestApplications() | ||||||
|  | 	suite.testUsers = testrig.NewTestUsers() | ||||||
|  | 	suite.testAccounts = testrig.NewTestAccounts() | ||||||
|  | 	suite.testAttachments = testrig.NewTestAttachments() | ||||||
|  | 	suite.testStatuses = testrig.NewTestStatuses() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *FollowRequestStandardTestSuite) SetupTest() { | ||||||
|  | 	testrig.InitTestLog() | ||||||
|  | 	suite.config = testrig.NewTestConfig() | ||||||
|  | 	suite.db = testrig.NewTestDB() | ||||||
|  | 	suite.storage = testrig.NewTestStorage() | ||||||
|  | 	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage) | ||||||
|  | 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) | ||||||
|  | 	suite.followRequestModule = followrequest.New(suite.config, suite.processor).(*followrequest.Module) | ||||||
|  | 	testrig.StandardDBSetup(suite.db, nil) | ||||||
|  | 	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *FollowRequestStandardTestSuite) TearDownTest() { | ||||||
|  | 	testrig.StandardDBTeardown(suite.db) | ||||||
|  | 	testrig.StandardStorageTeardown(suite.storage) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *FollowRequestStandardTestSuite) newContext(recorder *httptest.ResponseRecorder, requestMethod string, requestBody []byte, requestPath string, bodyContentType string) *gin.Context { | ||||||
|  | 	ctx, _ := gin.CreateTestContext(recorder) | ||||||
|  | 
 | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"])) | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) | ||||||
|  | 	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) | ||||||
|  | 
 | ||||||
|  | 	baseURI := fmt.Sprintf("%s://%s", suite.config.Protocol, suite.config.Host) | ||||||
|  | 	requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath) | ||||||
|  | 
 | ||||||
|  | 	ctx.Request = httptest.NewRequest(requestMethod, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting | ||||||
|  | 
 | ||||||
|  | 	if bodyContentType != "" { | ||||||
|  | 		ctx.Request.Header.Set("Content-Type", bodyContentType) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ctx | ||||||
|  | } | ||||||
|  | @ -26,13 +26,60 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FollowRequestGETHandler allows clients to get a list of their incoming follow requests. | // FollowRequestGETHandler swagger:operation GET /api/v1/follow_requests getFollowRequests | ||||||
|  | // | ||||||
|  | // Get an array of accounts that have requested to follow you. | ||||||
|  | // | ||||||
|  | // The next and previous queries can be parsed from the returned Link header. | ||||||
|  | // Example: | ||||||
|  | // | ||||||
|  | // ``` | ||||||
|  | // <https://example.org/api/v1/follow_requests?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/follow_requests?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev" | ||||||
|  | // ```` | ||||||
|  | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - follow_requests | ||||||
|  | // | ||||||
|  | // produces: | ||||||
|  | // - application/json | ||||||
|  | // | ||||||
|  | // parameters: | ||||||
|  | // - name: limit | ||||||
|  | //   type: integer | ||||||
|  | //   description: Number of accounts to return. | ||||||
|  | //   default: 40 | ||||||
|  | //   in: query | ||||||
|  | // | ||||||
|  | // security: | ||||||
|  | // - OAuth2 Bearer: | ||||||
|  | //   - read:follows | ||||||
|  | // | ||||||
|  | // responses: | ||||||
|  | //   '200': | ||||||
|  | //     headers: | ||||||
|  | //       Link: | ||||||
|  | //         type: string | ||||||
|  | //         description: Links to the next and previous queries. | ||||||
|  | //     schema: | ||||||
|  | //       type: array | ||||||
|  | //       items: | ||||||
|  | //         "$ref": "#/definitions/account" | ||||||
|  | //   '400': | ||||||
|  | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
| func (m *Module) FollowRequestGETHandler(c *gin.Context) { | func (m *Module) FollowRequestGETHandler(c *gin.Context) { | ||||||
| 	l := logrus.WithField("func", "statusCreatePOSTHandler") | 	l := logrus.WithField("func", "FollowRequestGETHandler") | ||||||
|  | 
 | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		l.Debugf("couldn't auth: %s", err) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										78
									
								
								internal/api/client/followrequest/get_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								internal/api/client/followrequest/get_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package followrequest_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type GetTestSuite struct { | ||||||
|  | 	FollowRequestStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *GetTestSuite) TestGet() { | ||||||
|  | 	requestingAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 	targetAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	// put a follow request in the database | ||||||
|  | 	fr := >smodel.FollowRequest{ | ||||||
|  | 		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", | ||||||
|  | 		CreatedAt:       time.Now(), | ||||||
|  | 		UpdatedAt:       time.Now(), | ||||||
|  | 		URI:             fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", requestingAccount.URI), | ||||||
|  | 		AccountID:       requestingAccount.ID, | ||||||
|  | 		TargetAccountID: targetAccount.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := suite.db.Put(context.Background(), fr) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodGet, []byte{}, "/api/v1/follow_requests", "") | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.followRequestModule.FollowRequestGETHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 1. we should have OK because our request was valid | ||||||
|  | 	suite.Equal(http.StatusOK, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	// 2. we should have no error message in the result body | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	// check the response | ||||||
|  | 	b, err := ioutil.ReadAll(result.Body) | ||||||
|  | 	assert.NoError(suite.T(), err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestGetTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, &GetTestSuite{}) | ||||||
|  | } | ||||||
|  | @ -19,21 +19,58 @@ | ||||||
| package followrequest | package followrequest | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/sirupsen/logrus" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FollowRequestAcceptPOSTHandler deals with follow request accepting. It should be served at | // FollowRequestRejectPOSTHandler swagger:operation POST /api/v1/follow_requests/{account_id}/reject rejectFollowRequest | ||||||
| // /api/v1/follow_requests/:id/authorize | // | ||||||
| func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) { | // Reject/deny follow request from the given account ID. | ||||||
| 	l := logrus.WithField("func", "statusCreatePOSTHandler") | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - follow_requests | ||||||
|  | // | ||||||
|  | // produces: | ||||||
|  | // - application/json | ||||||
|  | // | ||||||
|  | // parameters: | ||||||
|  | // - name: account_id | ||||||
|  | //   type: string | ||||||
|  | //   description: ID of the account requesting to follow you. | ||||||
|  | //   in: path | ||||||
|  | //   required: true | ||||||
|  | // | ||||||
|  | // security: | ||||||
|  | // - OAuth2 Bearer: | ||||||
|  | //   - write:follows | ||||||
|  | // | ||||||
|  | // responses: | ||||||
|  | //   '200': | ||||||
|  | //     name: account relationship | ||||||
|  | //     description: Your relationship to this account. | ||||||
|  | //     schema: | ||||||
|  | //       "$ref": "#/definitions/accountRelationship" | ||||||
|  | //   '400': | ||||||
|  | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | //   '403': | ||||||
|  | //      description: forbidden | ||||||
|  | //   '404': | ||||||
|  | //      description: not found | ||||||
|  | //   '500': | ||||||
|  | //      description: internal server error | ||||||
|  | func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) { | ||||||
|  | 	l := logrus.WithField("func", "FollowRequestRejectPOSTHandler") | ||||||
|  | 
 | ||||||
| 	authed, err := oauth.Authed(c, true, true, true, true) | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		l.Debugf("couldn't auth: %s", err) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -49,11 +86,12 @@ func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID) | 	relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		l.Debug(errWithCode.Error()) | 		l.Debug(errWithCode.Error()) | ||||||
| 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	c.JSON(http.StatusOK, r) | 
 | ||||||
|  | 	c.JSON(http.StatusOK, relationship) | ||||||
| } | } | ||||||
							
								
								
									
										87
									
								
								internal/api/client/followrequest/reject_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								internal/api/client/followrequest/reject_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package followrequest_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type RejectTestSuite struct { | ||||||
|  | 	FollowRequestStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *RejectTestSuite) TestReject() { | ||||||
|  | 	requestingAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 	targetAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	// put a follow request in the database | ||||||
|  | 	fr := >smodel.FollowRequest{ | ||||||
|  | 		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", | ||||||
|  | 		CreatedAt:       time.Now(), | ||||||
|  | 		UpdatedAt:       time.Now(), | ||||||
|  | 		URI:             fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", requestingAccount.URI), | ||||||
|  | 		AccountID:       requestingAccount.ID, | ||||||
|  | 		TargetAccountID: targetAccount.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := suite.db.Put(context.Background(), fr) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/reject", requestingAccount.ID), "") | ||||||
|  | 
 | ||||||
|  | 	ctx.Params = gin.Params{ | ||||||
|  | 		gin.Param{ | ||||||
|  | 			Key:   followrequest.IDKey, | ||||||
|  | 			Value: requestingAccount.ID, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.followRequestModule.FollowRequestRejectPOSTHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 1. we should have OK because our request was valid | ||||||
|  | 	suite.Equal(http.StatusOK, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	// 2. we should have no error message in the result body | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	// check the response | ||||||
|  | 	b, err := ioutil.ReadAll(result.Body) | ||||||
|  | 	assert.NoError(suite.T(), err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":false,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestRejectTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, &RejectTestSuite{}) | ||||||
|  | } | ||||||
|  | @ -255,6 +255,31 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, originAccountI | ||||||
| 	return follow, nil | 	return follow, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (r *relationshipDB) RejectFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, db.Error) { | ||||||
|  | 	// first get the follow request out of the database | ||||||
|  | 	fr := >smodel.FollowRequest{} | ||||||
|  | 	if err := r.conn. | ||||||
|  | 		NewSelect(). | ||||||
|  | 		Model(fr). | ||||||
|  | 		Where("account_id = ?", originAccountID). | ||||||
|  | 		Where("target_account_id = ?", targetAccountID). | ||||||
|  | 		Scan(ctx); err != nil { | ||||||
|  | 		return nil, r.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// now delete it from the database by ID | ||||||
|  | 	if _, err := r.conn. | ||||||
|  | 		NewDelete(). | ||||||
|  | 		Model(>smodel.FollowRequest{ID: fr.ID}). | ||||||
|  | 		WherePK(). | ||||||
|  | 		Exec(ctx); err != nil { | ||||||
|  | 		return nil, r.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// return the deleted follow request | ||||||
|  | 	return fr, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (r *relationshipDB) GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, db.Error) { | func (r *relationshipDB) GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, db.Error) { | ||||||
| 	followRequests := []*gtsmodel.FollowRequest{} | 	followRequests := []*gtsmodel.FollowRequest{} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -54,6 +54,11 @@ type Relationship interface { | ||||||
| 	// It will return the newly created follow for further processing. | 	// It will return the newly created follow for further processing. | ||||||
| 	AcceptFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.Follow, Error) | 	AcceptFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.Follow, Error) | ||||||
| 
 | 
 | ||||||
|  | 	// RejectFollowRequest fetches a follow request from the database, and then deletes it. | ||||||
|  | 	// | ||||||
|  | 	// The deleted follow request will be returned so that further processing can be done on it. | ||||||
|  | 	RejectFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, Error) | ||||||
|  | 
 | ||||||
| 	// GetAccountFollowRequests returns all follow requests targeting the given account. | 	// GetAccountFollowRequests returns all follow requests targeting the given account. | ||||||
| 	GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, Error) | 	GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, Error) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -35,6 +35,7 @@ type DB interface { | ||||||
| 	pub.Database | 	pub.Database | ||||||
| 	Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error | 	Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error | ||||||
| 	Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error | 	Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error | ||||||
|  | 	Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error | ||||||
| 	Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error | 	Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -63,10 +63,10 @@ func (suite *FederatingDBTestSuite) SetupSuite() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *FederatingDBTestSuite) SetupTest() { | func (suite *FederatingDBTestSuite) SetupTest() { | ||||||
|  | 	testrig.InitTestLog() | ||||||
| 	suite.config = testrig.NewTestConfig() | 	suite.config = testrig.NewTestConfig() | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||||
| 	testrig.InitTestLog() |  | ||||||
| 	suite.federatingDB = testrig.NewTestFederatingDB(suite.db) | 	suite.federatingDB = testrig.NewTestFederatingDB(suite.db) | ||||||
| 	testrig.StandardDBSetup(suite.db, suite.testAccounts) | 	testrig.StandardDBSetup(suite.db, suite.testAccounts) | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										119
									
								
								internal/federation/federatingdb/reject.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								internal/federation/federatingdb/reject.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,119 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package federatingdb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams/vocab" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (f *federatingDB) Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error { | ||||||
|  | 	l := logrus.WithFields( | ||||||
|  | 		logrus.Fields{ | ||||||
|  | 			"func": "Reject", | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	if logrus.GetLevel() >= logrus.DebugLevel { | ||||||
|  | 		i, err := marshalItem(reject) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		l = l.WithField("reject", i) | ||||||
|  | 		l.Debug("entering Reject") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	receivingAccount, _, fromFederatorChan := extractFromCtx(ctx) | ||||||
|  | 	if receivingAccount == nil || fromFederatorChan == nil { | ||||||
|  | 		// If the receiving account or federator channel wasn't set on the context, that means this request didn't pass | ||||||
|  | 		// through the API, but came from inside GtS as the result of another activity on this instance. That being so, | ||||||
|  | 		// we can safely just ignore this activity, since we know we've already processed it elsewhere. | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	rejectObject := reject.GetActivityStreamsObject() | ||||||
|  | 	if rejectObject == nil { | ||||||
|  | 		return errors.New("Reject: no object set on vocab.ActivityStreamsReject") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for iter := rejectObject.Begin(); iter != rejectObject.End(); iter = iter.Next() { | ||||||
|  | 		// check if the object is an IRI | ||||||
|  | 		if iter.IsIRI() { | ||||||
|  | 			// we have just the URI of whatever is being rejected, so we need to find out what it is | ||||||
|  | 			rejectedObjectIRI := iter.GetIRI() | ||||||
|  | 			if util.IsFollowPath(rejectedObjectIRI) { | ||||||
|  | 				// REJECT FOLLOW | ||||||
|  | 				gtsFollowRequest := >smodel.FollowRequest{} | ||||||
|  | 				if err := f.db.GetWhere(ctx, []db.Where{{Key: "uri", Value: rejectedObjectIRI.String()}}, gtsFollowRequest); err != nil { | ||||||
|  | 					return fmt.Errorf("Reject: couldn't get follow request with id %s from the database: %s", rejectedObjectIRI.String(), err) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// make sure the addressee of the original follow is the same as whatever inbox this landed in | ||||||
|  | 				if gtsFollowRequest.AccountID != receivingAccount.ID { | ||||||
|  | 					return errors.New("Reject: follow object account and inbox account were not the same") | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if _, err := f.db.RejectFollowRequest(ctx, gtsFollowRequest.AccountID, gtsFollowRequest.TargetAccountID); err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				return nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// check if iter is an AP object / type | ||||||
|  | 		if iter.GetType() == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		switch iter.GetType().GetTypeName() { | ||||||
|  | 		// we have the whole object so we can figure out what we're rejecting | ||||||
|  | 		case ap.ActivityFollow: | ||||||
|  | 			// REJECT FOLLOW | ||||||
|  | 			asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("Reject: couldn't parse follow into vocab.ActivityStreamsFollow") | ||||||
|  | 			} | ||||||
|  | 			// convert the follow to something we can understand | ||||||
|  | 			gtsFollow, err := f.typeConverter.ASFollowToFollow(ctx, asFollow) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return fmt.Errorf("Reject: error converting asfollow to gtsfollow: %s", err) | ||||||
|  | 			} | ||||||
|  | 			// make sure the addressee of the original follow is the same as whatever inbox this landed in | ||||||
|  | 			if gtsFollow.AccountID != receivingAccount.ID { | ||||||
|  | 				return errors.New("Reject: follow object account and inbox account were not the same") | ||||||
|  | 			} | ||||||
|  | 			if _, err := f.db.RejectFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										96
									
								
								internal/federation/federatingdb/reject_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								internal/federation/federatingdb/reject_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package federatingdb_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-fed/activity/streams" | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type RejectTestSuite struct { | ||||||
|  | 	FederatingDBTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *RejectTestSuite) TestRejectFollowRequest() { | ||||||
|  | 	// local_account_1 sent a follow request to remote_account_2; | ||||||
|  | 	// remote_account_2 rejects the follow request | ||||||
|  | 	followingAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 	followedAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 	fromFederatorChan := make(chan messages.FromFederator, 10) | ||||||
|  | 	ctx := createTestContext(followingAccount, followedAccount, fromFederatorChan) | ||||||
|  | 
 | ||||||
|  | 	// put the follow request in the database | ||||||
|  | 	fr := >smodel.FollowRequest{ | ||||||
|  | 		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", | ||||||
|  | 		CreatedAt:       time.Now(), | ||||||
|  | 		UpdatedAt:       time.Now(), | ||||||
|  | 		URI:             util.GenerateURIForFollow(followingAccount.Username, "http", "localhost:8080", "01FJ1S8DX3STJJ6CEYPMZ1M0R3"), | ||||||
|  | 		AccountID:       followingAccount.ID, | ||||||
|  | 		TargetAccountID: followedAccount.ID, | ||||||
|  | 	} | ||||||
|  | 	err := suite.db.Put(ctx, fr) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	asFollow, err := suite.tc.FollowToAS(ctx, suite.tc.FollowRequestToFollow(ctx, fr), followingAccount, followedAccount) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	rejectingAccountURI := testrig.URLMustParse(followedAccount.URI) | ||||||
|  | 	requestingAccountURI := testrig.URLMustParse(followingAccount.URI) | ||||||
|  | 
 | ||||||
|  | 	// create a Reject | ||||||
|  | 	reject := streams.NewActivityStreamsReject() | ||||||
|  | 
 | ||||||
|  | 	// set the rejecting actor on it | ||||||
|  | 	acceptActorProp := streams.NewActivityStreamsActorProperty() | ||||||
|  | 	acceptActorProp.AppendIRI(rejectingAccountURI) | ||||||
|  | 	reject.SetActivityStreamsActor(acceptActorProp) | ||||||
|  | 
 | ||||||
|  | 	// Set the recreated follow as the 'object' property. | ||||||
|  | 	acceptObject := streams.NewActivityStreamsObjectProperty() | ||||||
|  | 	acceptObject.AppendActivityStreamsFollow(asFollow) | ||||||
|  | 	reject.SetActivityStreamsObject(acceptObject) | ||||||
|  | 
 | ||||||
|  | 	// Set the To of the reject as the originator of the follow | ||||||
|  | 	acceptTo := streams.NewActivityStreamsToProperty() | ||||||
|  | 	acceptTo.AppendIRI(requestingAccountURI) | ||||||
|  | 	reject.SetActivityStreamsTo(acceptTo) | ||||||
|  | 
 | ||||||
|  | 	// process the reject in the federating database | ||||||
|  | 	err = suite.federatingDB.Reject(ctx, reject) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	// there should be nothing in the federator channel since nothing needs to be passed | ||||||
|  | 	suite.Empty(fromFederatorChan) | ||||||
|  | 
 | ||||||
|  | 	// the follow request should not be in the database anymore -- it's been rejected | ||||||
|  | 	err = suite.db.GetByID(ctx, fr.ID, >smodel.FollowRequest{}) | ||||||
|  | 	suite.ErrorIs(err, db.ErrNoEntries) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestRejectTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, &RejectTestSuite{}) | ||||||
|  | } | ||||||
|  | @ -250,16 +250,17 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa | ||||||
| 		OnFollow: pub.OnFollowDoNothing, | 		OnFollow: pub.OnFollowDoNothing, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// override some default behaviors and trigger our own side effects | ||||||
| 	other = []interface{}{ | 	other = []interface{}{ | ||||||
| 		// override default undo behavior and trigger our own side effects |  | ||||||
| 		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { | 		func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { | ||||||
| 			return f.FederatingDB().Undo(ctx, undo) | 			return f.FederatingDB().Undo(ctx, undo) | ||||||
| 		}, | 		}, | ||||||
| 		// override default accept behavior and trigger our own side effects |  | ||||||
| 		func(ctx context.Context, accept vocab.ActivityStreamsAccept) error { | 		func(ctx context.Context, accept vocab.ActivityStreamsAccept) error { | ||||||
| 			return f.FederatingDB().Accept(ctx, accept) | 			return f.FederatingDB().Accept(ctx, accept) | ||||||
| 		}, | 		}, | ||||||
| 		// override default announce behavior and trigger our own side effects | 		func(ctx context.Context, reject vocab.ActivityStreamsReject) error { | ||||||
|  | 			return f.FederatingDB().Reject(ctx, reject) | ||||||
|  | 		}, | ||||||
| 		func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error { | 		func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error { | ||||||
| 			return f.FederatingDB().Announce(ctx, announce) | 			return f.FederatingDB().Announce(ctx, announce) | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -99,6 +99,45 @@ func (p *processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a | ||||||
| 	return r, nil | 	return r, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) FollowRequestDeny(ctx context.Context, auth *oauth.Auth) gtserror.WithCode { | func (p *processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { | ||||||
| 	return nil | 	followRequest, err := p.db.RejectFollowRequest(ctx, accountID, auth.Account.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, gtserror.NewErrorNotFound(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if followRequest.Account == nil { | ||||||
|  | 		a, err := p.db.GetAccountByID(ctx, followRequest.AccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 		followRequest.Account = a | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if followRequest.TargetAccount == nil { | ||||||
|  | 		a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 		followRequest.TargetAccount = a | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	p.fromClientAPI <- messages.FromClientAPI{ | ||||||
|  | 		APObjectType:   ap.ActivityFollow, | ||||||
|  | 		APActivityType: ap.ActivityReject, | ||||||
|  | 		GTSModel:       followRequest, | ||||||
|  | 		OriginAccount:  followRequest.Account, | ||||||
|  | 		TargetAccount:  followRequest.TargetAccount, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	gtsR, err := p.db.GetRelationship(ctx, auth.Account.ID, accountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return r, nil | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										143
									
								
								internal/processing/followrequest_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								internal/processing/followrequest_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,143 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package processing_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type FollowRequestTestSuite struct { | ||||||
|  | 	ProcessingStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *FollowRequestTestSuite) TestFollowRequestAccept() { | ||||||
|  | 	requestingAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 	targetAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	// put a follow request in the database | ||||||
|  | 	fr := >smodel.FollowRequest{ | ||||||
|  | 		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", | ||||||
|  | 		CreatedAt:       time.Now(), | ||||||
|  | 		UpdatedAt:       time.Now(), | ||||||
|  | 		URI:             fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", requestingAccount.URI), | ||||||
|  | 		AccountID:       requestingAccount.ID, | ||||||
|  | 		TargetAccountID: targetAccount.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := suite.db.Put(context.Background(), fr) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	relationship, errWithCode := suite.processor.FollowRequestAccept(context.Background(), suite.testAutheds["local_account_1"], requestingAccount.ID) | ||||||
|  | 	suite.NoError(errWithCode) | ||||||
|  | 	suite.EqualValues(&apimodel.Relationship{ID: "01FHMQX3GAABWSM0S2VZEC2SWC", Following: false, ShowingReblogs: false, Notifying: false, FollowedBy: true, Blocking: false, BlockedBy: false, Muting: false, MutingNotifications: false, Requested: false, DomainBlocking: false, Endorsed: false, Note: ""}, relationship) | ||||||
|  | 	time.Sleep(1 * time.Second) | ||||||
|  | 
 | ||||||
|  | 	// accept should be sent to some_user | ||||||
|  | 	sent, ok := suite.sentHTTPRequests[requestingAccount.InboxURI] | ||||||
|  | 	suite.True(ok) | ||||||
|  | 
 | ||||||
|  | 	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(sent, accept) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(targetAccount.URI, accept.Actor) | ||||||
|  | 	suite.Equal(requestingAccount.URI, accept.Object.Actor) | ||||||
|  | 	suite.Equal(fr.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(requestingAccount.URI, accept.To) | ||||||
|  | 	suite.Equal("Accept", accept.Type) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *FollowRequestTestSuite) TestFollowRequestReject() { | ||||||
|  | 	requestingAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 	targetAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	// put a follow request in the database | ||||||
|  | 	fr := >smodel.FollowRequest{ | ||||||
|  | 		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", | ||||||
|  | 		CreatedAt:       time.Now(), | ||||||
|  | 		UpdatedAt:       time.Now(), | ||||||
|  | 		URI:             fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", requestingAccount.URI), | ||||||
|  | 		AccountID:       requestingAccount.ID, | ||||||
|  | 		TargetAccountID: targetAccount.ID, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := suite.db.Put(context.Background(), fr) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	relationship, errWithCode := suite.processor.FollowRequestReject(context.Background(), suite.testAutheds["local_account_1"], requestingAccount.ID) | ||||||
|  | 	suite.NoError(errWithCode) | ||||||
|  | 	suite.EqualValues(&apimodel.Relationship{ID: "01FHMQX3GAABWSM0S2VZEC2SWC", Following: false, ShowingReblogs: false, Notifying: false, FollowedBy: false, Blocking: false, BlockedBy: false, Muting: false, MutingNotifications: false, Requested: false, DomainBlocking: false, Endorsed: false, Note: ""}, relationship) | ||||||
|  | 	time.Sleep(1 * time.Second) | ||||||
|  | 
 | ||||||
|  | 	// reject should be sent to some_user | ||||||
|  | 	sent, ok := suite.sentHTTPRequests[requestingAccount.InboxURI] | ||||||
|  | 	suite.True(ok) | ||||||
|  | 
 | ||||||
|  | 	reject := &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(sent, reject) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(targetAccount.URI, reject.Actor) | ||||||
|  | 	suite.Equal(requestingAccount.URI, reject.Object.Actor) | ||||||
|  | 	suite.Equal(fr.URI, reject.Object.ID) | ||||||
|  | 	suite.Equal(targetAccount.URI, reject.Object.Object) | ||||||
|  | 	suite.Equal(targetAccount.URI, reject.Object.To) | ||||||
|  | 	suite.Equal("Follow", reject.Object.Type) | ||||||
|  | 	suite.Equal(requestingAccount.URI, reject.To) | ||||||
|  | 	suite.Equal("Reject", reject.Type) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestFollowRequestTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, &FollowRequestTestSuite{}) | ||||||
|  | } | ||||||
|  | @ -140,7 +140,19 @@ func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			return p.federateAcceptFollowRequest(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount) | 			return p.federateAcceptFollowRequest(ctx, follow) | ||||||
|  | 		} | ||||||
|  | 	case ap.ActivityReject: | ||||||
|  | 		// REJECT | ||||||
|  | 		switch clientMsg.APObjectType { | ||||||
|  | 		case ap.ActivityFollow: | ||||||
|  | 			// REJECT FOLLOW (request) | ||||||
|  | 			followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) | ||||||
|  | 			if !ok { | ||||||
|  | 				return errors.New("reject was not parseable as *gtsmodel.FollowRequest") | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return p.federateRejectFollowRequest(ctx, followRequest) | ||||||
| 		} | 		} | ||||||
| 	case ap.ActivityUndo: | 	case ap.ActivityUndo: | ||||||
| 		// UNDO | 		// UNDO | ||||||
|  | @ -453,7 +465,30 @@ func (p *processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Stat | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *processor) federateAcceptFollowRequest(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | func (p *processor) federateAcceptFollowRequest(ctx context.Context, follow *gtsmodel.Follow) error { | ||||||
|  | 	if follow.Account == nil { | ||||||
|  | 		a, err := p.db.GetAccountByID(ctx, follow.AccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		follow.Account = a | ||||||
|  | 	} | ||||||
|  | 	originAccount := follow.Account | ||||||
|  | 
 | ||||||
|  | 	if follow.TargetAccount == nil { | ||||||
|  | 		a, err := p.db.GetAccountByID(ctx, follow.TargetAccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		follow.TargetAccount = a | ||||||
|  | 	} | ||||||
|  | 	targetAccount := follow.TargetAccount | ||||||
|  | 
 | ||||||
|  | 	// if target account isn't from our domain we shouldn't do anything | ||||||
|  | 	if targetAccount.Domain != "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// if both accounts are local there's nothing to do here | 	// if both accounts are local there's nothing to do here | ||||||
| 	if originAccount.Domain == "" && targetAccount.Domain == "" { | 	if originAccount.Domain == "" && targetAccount.Domain == "" { | ||||||
| 		return nil | 		return nil | ||||||
|  | @ -503,6 +538,80 @@ func (p *processor) federateAcceptFollowRequest(ctx context.Context, follow *gts | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (p *processor) federateRejectFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { | ||||||
|  | 	if followRequest.Account == nil { | ||||||
|  | 		a, err := p.db.GetAccountByID(ctx, followRequest.AccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		followRequest.Account = a | ||||||
|  | 	} | ||||||
|  | 	originAccount := followRequest.Account | ||||||
|  | 
 | ||||||
|  | 	if followRequest.TargetAccount == nil { | ||||||
|  | 		a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		followRequest.TargetAccount = a | ||||||
|  | 	} | ||||||
|  | 	targetAccount := followRequest.TargetAccount | ||||||
|  | 
 | ||||||
|  | 	// if target account isn't from our domain we shouldn't do anything | ||||||
|  | 	if targetAccount.Domain != "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// if both accounts are local there's nothing to do here | ||||||
|  | 	if originAccount.Domain == "" && targetAccount.Domain == "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// recreate the AS follow | ||||||
|  | 	follow := p.tc.FollowRequestToFollow(ctx, followRequest) | ||||||
|  | 	asFollow, err := p.tc.FollowToAS(ctx, follow, originAccount, targetAccount) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	rejectingAccountURI, err := url.Parse(targetAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	requestingAccountURI, err := url.Parse(originAccount.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// create a Reject | ||||||
|  | 	reject := streams.NewActivityStreamsReject() | ||||||
|  | 
 | ||||||
|  | 	// set the rejecting actor on it | ||||||
|  | 	acceptActorProp := streams.NewActivityStreamsActorProperty() | ||||||
|  | 	acceptActorProp.AppendIRI(rejectingAccountURI) | ||||||
|  | 	reject.SetActivityStreamsActor(acceptActorProp) | ||||||
|  | 
 | ||||||
|  | 	// Set the recreated follow as the 'object' property. | ||||||
|  | 	acceptObject := streams.NewActivityStreamsObjectProperty() | ||||||
|  | 	acceptObject.AppendActivityStreamsFollow(asFollow) | ||||||
|  | 	reject.SetActivityStreamsObject(acceptObject) | ||||||
|  | 
 | ||||||
|  | 	// Set the To of the reject as the originator of the follow | ||||||
|  | 	acceptTo := streams.NewActivityStreamsToProperty() | ||||||
|  | 	acceptTo.AppendIRI(requestingAccountURI) | ||||||
|  | 	reject.SetActivityStreamsTo(acceptTo) | ||||||
|  | 
 | ||||||
|  | 	outboxIRI, err := url.Parse(targetAccount.OutboxURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateRejectFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// send off the reject using the rejecting account's outbox | ||||||
|  | 	_, err = p.federator.FederatingActor().Send(ctx, outboxIRI, reject) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (p *processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | func (p *processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { | ||||||
| 	// if both accounts are local there's nothing to do here | 	// if both accounts are local there's nothing to do here | ||||||
| 	if originAccount.Domain == "" && targetAccount.Domain == "" { | 	if originAccount.Domain == "" && targetAccount.Domain == "" { | ||||||
|  |  | ||||||
|  | @ -161,22 +161,13 @@ func (p *processor) processCreateFollowRequestFromFederator(ctx context.Context, | ||||||
| 		return p.notifyFollowRequest(ctx, followRequest) | 		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 | 	// 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) | 	follow, err := p.db.AcceptFollowRequest(ctx, followRequest.AccountID, followRequest.TargetAccountID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := p.federateAcceptFollowRequest(ctx, follow, originAccount, targetAccount); err != nil { | 	if err := p.federateAcceptFollowRequest(ctx, follow); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -118,8 +118,10 @@ type Processor interface { | ||||||
| 
 | 
 | ||||||
| 	// FollowRequestsGet handles the getting of the authed account's incoming follow requests | 	// FollowRequestsGet handles the getting of the authed account's incoming follow requests | ||||||
| 	FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) | 	FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) | ||||||
| 	// FollowRequestAccept handles the acceptance of a follow request from the given account ID | 	// FollowRequestAccept handles the acceptance of a follow request from the given account ID. | ||||||
| 	FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) | 	FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) | ||||||
|  | 	// FollowRequestReject handles the rejection of a follow request from the given account ID. | ||||||
|  | 	FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) | ||||||
| 
 | 
 | ||||||
| 	// InstanceGet retrieves instance information for serving at api/v1/instance | 	// InstanceGet retrieves instance information for serving at api/v1/instance | ||||||
| 	InstanceGet(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) | 	InstanceGet(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) | ||||||
|  |  | ||||||
|  | @ -96,9 +96,9 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *ProcessingStandardTestSuite) SetupTest() { | func (suite *ProcessingStandardTestSuite) SetupTest() { | ||||||
|  | 	testrig.InitTestLog() | ||||||
| 	suite.config = testrig.NewTestConfig() | 	suite.config = testrig.NewTestConfig() | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	testrig.InitTestLog() |  | ||||||
| 	suite.storage = testrig.NewTestStorage() | 	suite.storage = testrig.NewTestStorage() | ||||||
| 	suite.typeconverter = testrig.NewTestTypeConverter(suite.db) | 	suite.typeconverter = testrig.NewTestTypeConverter(suite.db) | ||||||
| 
 | 
 | ||||||
|  | @ -149,6 +149,38 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { | ||||||
| 			return response, nil | 			return response, nil | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		if req.URL.String() == suite.testAccounts["remote_account_2"].URI { | ||||||
|  | 			// the request is for remote account 2 | ||||||
|  | 			someAccount := suite.testAccounts["remote_account_2"] | ||||||
|  | 
 | ||||||
|  | 			someAccountAS, err := suite.typeconverter.AccountToAS(context.Background(), someAccount) | ||||||
|  | 			if err != nil { | ||||||
|  | 				panic(err) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			someAccountI, err := streams.Serialize(someAccountAS) | ||||||
|  | 			if err != nil { | ||||||
|  | 				panic(err) | ||||||
|  | 			} | ||||||
|  | 			someAccountJson, err := json.Marshal(someAccountI) | ||||||
|  | 			if err != nil { | ||||||
|  | 				panic(err) | ||||||
|  | 			} | ||||||
|  | 			responseType := "application/activity+json" | ||||||
|  | 
 | ||||||
|  | 			reader := bytes.NewReader(someAccountJson) | ||||||
|  | 			readCloser := io.NopCloser(reader) | ||||||
|  | 			response := &http.Response{ | ||||||
|  | 				StatusCode:    200, | ||||||
|  | 				Body:          readCloser, | ||||||
|  | 				ContentLength: int64(len(someAccountJson)), | ||||||
|  | 				Header: http.Header{ | ||||||
|  | 					"content-type": {responseType}, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 			return response, nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		if req.URL.String() == "http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1" { | 		if req.URL.String() == "http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1" { | ||||||
| 			// the request is for the forwarded message | 			// the request is for the forwarded message | ||||||
| 			message := suite.testActivities["forwarded_message"].Activity.GetActivityStreamsObject().At(0).GetActivityStreamsNote() | 			message := suite.testActivities["forwarded_message"].Activity.GetActivityStreamsObject().At(0).GetActivityStreamsNote() | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue