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:
tobi 2021-10-16 13:27:43 +02:00 committed by GitHub
parent 107685e22e
commit 15621f5324
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1256 additions and 69 deletions

View file

@ -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

View file

@ -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: |-

View 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)
}

View 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 := &gtsmodel.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{})
}

View file

@ -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) {
}

View file

@ -28,21 +28,20 @@
) )
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
} }

View 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
}

View file

@ -26,13 +26,60 @@
"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
} }

View 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 := &gtsmodel.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{})
}

View file

@ -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)
} }

View 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 := &gtsmodel.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{})
}

View file

@ -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 := &gtsmodel.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(&gtsmodel.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{}

View file

@ -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)

View file

@ -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
} }

View file

@ -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)
} }

View 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 := &gtsmodel.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
}

View 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 := &gtsmodel.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, &gtsmodel.FollowRequest{})
suite.ErrorIs(err, db.ErrNoEntries)
}
func TestRejectTestSuite(t *testing.T) {
suite.Run(t, &RejectTestSuite{})
}

View file

@ -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)
}, },

View file

@ -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
} }

View 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 := &gtsmodel.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 := &gtsmodel.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{})
}

View file

@ -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 == "" {

View file

@ -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
} }

View file

@ -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)

View file

@ -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()