Skip to content

Commit

Permalink
[feature] Implement reports admin API so admins can view + close repo…
Browse files Browse the repository at this point in the history
…rts (#1378)

* add admin report api endpoints + tests

* [chore] remove funky duplicate attachment in testrig
  • Loading branch information
tsmethurst authored Jan 25, 2023
1 parent 27d4e36 commit faeb7de
Show file tree
Hide file tree
Showing 20 changed files with 2,674 additions and 72 deletions.
379 changes: 319 additions & 60 deletions docs/api/swagger.yaml

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion internal/api/client/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ const (
AccountsActionPath = AccountsPathWithID + "/action"
MediaCleanupPath = BasePath + "/media_cleanup"
MediaRefetchPath = BasePath + "/media_refetch"
// ReportsPath is for serving admin view of user reports.
ReportsPath = BasePath + "/reports"
// ReportsPathWithID is for viewing/acting on one report.
ReportsPathWithID = ReportsPath + "/:" + IDKey
// ReportsResolvePath is for marking one report as resolved.
ReportsResolvePath = ReportsPathWithID + "/resolve"

// ExportQueryKey is for requesting a public export of some data.
ExportQueryKey = "export"
Expand All @@ -65,6 +71,15 @@ const (
LimitKey = "limit"
// DomainQueryKey is for specifying a domain during admin actions.
DomainQueryKey = "domain"
// ResolvedKey is for filtering reports by their resolved status
ResolvedKey = "resolved"
// AccountIDKey is for selecting account in API paths.
AccountIDKey = "account_id"
// TargetAccountIDKey is for selecting target account in API paths.
TargetAccountIDKey = "target_account_id"
MaxIDKey = "max_id"
SinceIDKey = "since_id"
MinIDKey = "min_id"
)

type Module struct {
Expand All @@ -78,17 +93,29 @@ func New(processor processing.Processor) *Module {
}

func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
// emoji stuff
attachHandler(http.MethodPost, EmojiPath, m.EmojiCreatePOSTHandler)
attachHandler(http.MethodGet, EmojiPath, m.EmojisGETHandler)
attachHandler(http.MethodDelete, EmojiPathWithID, m.EmojiDELETEHandler)
attachHandler(http.MethodGet, EmojiPathWithID, m.EmojiGETHandler)
attachHandler(http.MethodPatch, EmojiPathWithID, m.EmojiPATCHHandler)
attachHandler(http.MethodGet, EmojiCategoriesPath, m.EmojiCategoriesGETHandler)

// domain block stuff
attachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler)
attachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler)
attachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
attachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler)

// accounts stuff
attachHandler(http.MethodPost, AccountsActionPath, m.AccountActionPOSTHandler)

// media stuff
attachHandler(http.MethodPost, MediaCleanupPath, m.MediaCleanupPOSTHandler)
attachHandler(http.MethodPost, MediaRefetchPath, m.MediaRefetchPOSTHandler)
attachHandler(http.MethodGet, EmojiCategoriesPath, m.EmojiCategoriesGETHandler)

// reports stuff
attachHandler(http.MethodGet, ReportsPath, m.ReportsGETHandler)
attachHandler(http.MethodGet, ReportsPathWithID, m.ReportGETHandler)
attachHandler(http.MethodPost, ReportsResolvePath, m.ReportResolvePOSTHandler)
}
2 changes: 2 additions & 0 deletions internal/api/client/admin/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type AdminStandardTestSuite struct {
testStatuses map[string]*gtsmodel.Status
testEmojis map[string]*gtsmodel.Emoji
testEmojiCategories map[string]*gtsmodel.EmojiCategory
testReports map[string]*gtsmodel.Report

// module being tested
adminModule *admin.Module
Expand All @@ -77,6 +78,7 @@ func (suite *AdminStandardTestSuite) SetupSuite() {
suite.testStatuses = testrig.NewTestStatuses()
suite.testEmojis = testrig.NewTestEmojis()
suite.testEmojiCategories = testrig.NewTestEmojiCategories()
suite.testReports = testrig.NewTestReports()
}

func (suite *AdminStandardTestSuite) SetupTest() {
Expand Down
103 changes: 103 additions & 0 deletions internal/api/client/admin/reportget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
GoToSocial
Copyright (C) 2021-2023 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 admin

import (
"errors"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)

// ReportGETHandler swagger:operation GET /api/v1/admin/reports/{id} adminReportGet
//
// View user moderation report with the given id.
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// type: string
// description: The id of the report.
// in: path
// required: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// name: report
// description: The requested report.
// schema:
// "$ref": "#/definitions/adminReport"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) ReportGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}

if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}

if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}

reportID := c.Param(IDKey)
if reportID == "" {
err := errors.New("no report id specified")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}

report, errWithCode := m.processor.AdminReportGet(c.Request.Context(), authed, reportID)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}

c.JSON(http.StatusOK, report)
}
125 changes: 125 additions & 0 deletions internal/api/client/admin/reportresolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
GoToSocial
Copyright (C) 2021-2023 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 admin

import (
"errors"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)

// ReportResolvePOSTHandler swagger:operation POST /api/v1/admin/reports/{id}/resolve adminReportResolve
//
// Mark a report as resolved.
//
// ---
// tags:
// - admin
//
// consumes:
// - application/json
// - application/xml
// - multipart/form-data
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// type: string
// description: The id of the report.
// in: path
// required: true
// -
// name: action_taken_comment
// in: formData
// description: >-
// Optional admin comment on the action taken in response to this report.
// Useful for providing an explanation about what action was taken (if any)
// before the report was marked as resolved. This will be visible to the user
// that created the report!
// type: string
// example: The reported account was suspended.
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// name: report
// description: The resolved report.
// schema:
// "$ref": "#/definitions/adminReport"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) ReportResolvePOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}

if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}

if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}

reportID := c.Param(IDKey)
if reportID == "" {
err := errors.New("no report id specified")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}

form := &apimodel.AdminReportResolveRequest{}
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}

report, errWithCode := m.processor.AdminReportResolve(c.Request.Context(), authed, reportID, form.ActionTakenComment)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}

c.JSON(http.StatusOK, report)
}
Loading

0 comments on commit faeb7de

Please sign in to comment.