Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Follow request improvements #282

Merged
merged 15 commits into from
Oct 16, 2021
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

`swagger generate spec -o docs/api/swagger.yaml --scan-models`
```bash
swagger generate spec -o docs/api/swagger.yaml --scan-models
```

## CI/CD configuration

Expand Down
109 changes: 109 additions & 0 deletions docs/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,115 @@ paths:
summary: Get an array of accounts that requesting account has blocked.
tags:
- 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:
get:
description: |-
Expand Down
99 changes: 99 additions & 0 deletions internal/api/client/followrequest/authorize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors [email protected]

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 changes: 87 additions & 0 deletions internal/api/client/followrequest/authorize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors [email protected]

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{})
}
27 changes: 0 additions & 27 deletions internal/api/client/followrequest/deny.go

This file was deleted.

19 changes: 9 additions & 10 deletions internal/api/client/followrequest/followrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,20 @@ import (
)

const (
// IDKey is for status UUIDs
// IDKey is for account IDs
IDKey = "id"
// BasePath is the base path for serving the follow request API
BasePath = "/api/v1/follow_requests"
// 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

// AcceptPath is used for accepting follow requests
AcceptPath = BasePathWithID + "/authorize"
// DenyPath is used for denying follow requests
DenyPath = BasePathWithID + "/reject"
// AuthorizePath is used for authorizing follow requests
AuthorizePath = BasePathWithID + "/authorize"
// RejectPath is used for rejecting follow requests
RejectPath = BasePathWithID + "/reject"
)

// Module implements the ClientAPIModule interface for every related to interacting with follow requests
// Module implements the ClientAPIModule interface
type Module struct {
config *config.Config
processor processing.Processor
Expand All @@ -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
func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodGet, BasePath, m.FollowRequestGETHandler)
r.AttachHandler(http.MethodPost, AcceptPath, m.FollowRequestAcceptPOSTHandler)
r.AttachHandler(http.MethodPost, DenyPath, m.FollowRequestDenyPOSTHandler)
r.AttachHandler(http.MethodPost, AuthorizePath, m.FollowRequestAuthorizePOSTHandler)
r.AttachHandler(http.MethodPost, RejectPath, m.FollowRequestRejectPOSTHandler)
return nil
}
Loading