commit faeb7ded3b5d595910f424fd9cf9c6fe5935e648
parent 27d4e364e04c65a11c2855eaa9a1ffc55eb23239
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 25 Jan 2023 11:12:17 +0100
[feature] Implement reports admin API so admins can view + close reports (#1378)
* add admin report api endpoints + tests
* [chore] remove funky duplicate attachment in testrig
Diffstat:
20 files changed, 2674 insertions(+), 72 deletions(-)
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
@@ -356,6 +356,103 @@ definitions:
type: object
x-go-name: Relationship
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ adminAccountInfo:
+ properties:
+ account:
+ $ref: '#/definitions/account'
+ approved:
+ description: Whether the account is currently approved.
+ type: boolean
+ x-go-name: Approved
+ confirmed:
+ description: Whether the account has confirmed their email address.
+ type: boolean
+ x-go-name: Confirmed
+ created_at:
+ description: When the account was first discovered. (ISO 8601 Datetime)
+ example: "2021-07-30T09:20:25+00:00"
+ type: string
+ x-go-name: CreatedAt
+ created_by_application_id:
+ description: The ID of the application that created this account.
+ type: string
+ x-go-name: CreatedByApplicationID
+ disabled:
+ description: Whether the account is currently disabled.
+ type: boolean
+ x-go-name: Disabled
+ domain:
+ description: |-
+ The domain of the account.
+ Null for local accounts.
+ example: example.org
+ type: string
+ x-go-name: Domain
+ email:
+ description: |-
+ The email address associated with the account.
+ Empty string for remote accounts or accounts with
+ no known email address.
+ example: someone@somewhere.com
+ type: string
+ x-go-name: Email
+ id:
+ description: The ID of the account in the database.
+ example: 01GQ4PHNT622DQ9X95XQX4KKNR
+ type: string
+ x-go-name: ID
+ invite_request:
+ description: |-
+ The reason given when requesting an invite.
+ Null if not known / remote account.
+ example: Pleaaaaaaaaaaaaaaase!!
+ type: string
+ x-go-name: InviteRequest
+ invited_by_account_id:
+ description: The ID of the account that invited this user
+ type: string
+ x-go-name: InvitedByAccountID
+ ip:
+ description: |-
+ The IP address last used to login to this account.
+ Null if not known.
+ example: 192.0.2.1
+ type: string
+ x-go-name: IP
+ ips:
+ description: |-
+ All known IP addresses associated with this account.
+ NOT IMPLEMENTED (will always be empty array).
+ example: []
+ items: {}
+ type: array
+ x-go-name: IPs
+ locale:
+ description: The locale of the account. (ISO 639 Part 1 two-letter language code)
+ example: en
+ type: string
+ x-go-name: Locale
+ role:
+ description: The current role of the account.
+ type: string
+ x-go-name: Role
+ silenced:
+ description: Whether the account is currently silenced
+ type: boolean
+ x-go-name: Silenced
+ suspended:
+ description: Whether the account is currently suspended.
+ type: boolean
+ x-go-name: Suspended
+ username:
+ description: The username of the account.
+ example: dril
+ type: string
+ x-go-name: Username
+ title: AdminAccountInfo models the admin view of an account's details.
+ type: object
+ x-go-name: AdminAccountInfo
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
adminEmoji:
properties:
category:
@@ -423,6 +520,86 @@ definitions:
type: object
x-go-name: AdminEmoji
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ adminReport:
+ properties:
+ account:
+ $ref: '#/definitions/adminAccountInfo'
+ action_taken:
+ description: Whether an action has been taken by an admin in response to this report.
+ example: false
+ type: boolean
+ x-go-name: ActionTaken
+ action_taken_at:
+ description: |-
+ If an action was taken, at what time was this done? (ISO 8601 Datetime)
+ Will be null if not set / no action yet taken.
+ example: "2021-07-30T09:20:25+00:00"
+ type: string
+ x-go-name: ActionTakenAt
+ action_taken_by_account:
+ $ref: '#/definitions/adminAccountInfo'
+ action_taken_comment:
+ description: |-
+ If an action was taken, what comment was made by the admin on the taken action?
+ Will be null if not set / no action yet taken.
+ example: Account was suspended.
+ type: string
+ x-go-name: ActionTakenComment
+ assigned_account:
+ $ref: '#/definitions/adminAccountInfo'
+ category:
+ description: Under what category was this report created?
+ example: spam
+ type: string
+ x-go-name: Category
+ comment:
+ description: |-
+ Comment submitted when the report was created.
+ Will be empty if no comment was submitted.
+ example: This person has been harassing me.
+ type: string
+ x-go-name: Comment
+ created_at:
+ description: The date when this report was created (ISO 8601 Datetime).
+ example: "2021-07-30T09:20:25+00:00"
+ type: string
+ x-go-name: CreatedAt
+ forwarded:
+ description: Bool to indicate that report should be federated to remote instance.
+ example: true
+ type: boolean
+ x-go-name: Forwarded
+ id:
+ description: ID of the report.
+ example: 01FBVD42CQ3ZEEVMW180SBX03B
+ type: string
+ x-go-name: ID
+ rule_ids:
+ description: |-
+ Array of rule IDs that were submitted along with this report.
+ NOT IMPLEMENTED, will always be empty array.
+ items: {}
+ type: array
+ x-go-name: Rules
+ statuses:
+ description: |-
+ Array of statuses that were submitted along with this report.
+ Will be empty if no status IDs were submitted with the report.
+ items:
+ $ref: '#/definitions/status'
+ type: array
+ x-go-name: Statuses
+ target_account:
+ $ref: '#/definitions/adminAccountInfo'
+ updated_at:
+ description: Time of last action on this report (ISO 8601 Datetime).
+ example: "2021-07-30T09:20:25+00:00"
+ type: string
+ x-go-name: UpdatedAt
+ title: AdminReport models the admin view of a report.
+ type: object
+ x-go-name: AdminReport
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
advancedVisibilityFlagsForm:
description: |-
AdvancedVisibilityFlagsForm allows a few more advanced flags to be set on new statuses, in addition
@@ -1278,71 +1455,12 @@ definitions:
mediaMeta:
description: This can be metadata about an image, an audio file, video, etc.
properties:
- aspect:
- description: |-
- Aspect ratio of the media.
- Equal to width / height.
- example: 1.777777778
- format: float
- type: number
- x-go-name: Aspect
- audio_bitrate:
- type: string
- x-go-name: AudioBitrate
- audio_channels:
- type: string
- x-go-name: AudioChannels
- audio_encode:
- type: string
- x-go-name: AudioEncode
- duration:
- description: |-
- Duration of the media in seconds.
- Only set for video and audio.
- example: 5.43
- format: float
- type: number
- x-go-name: Duration
focus:
$ref: '#/definitions/mediaFocus'
- fps:
- description: |-
- Framerate of the media.
- Only set for video and gifs.
- example: 30
- format: uint16
- type: integer
- x-go-name: FPS
- height:
- description: |-
- Height of the media in pixels.
- Not set for audio.
- example: 1080
- format: int64
- type: integer
- x-go-name: Height
- length:
- type: string
- x-go-name: Length
original:
$ref: '#/definitions/mediaDimensions'
- size:
- description: |-
- Size of the media, in the format `[width]x[height]`.
- Not set for audio.
- example: 1920x1080
- type: string
- x-go-name: Size
small:
$ref: '#/definitions/mediaDimensions'
- width:
- description: |-
- Width of the media in pixels.
- Not set for audio.
- example: 1920
- format: int64
- type: integer
- x-go-name: Width
title: MediaMeta models media metadata.
type: object
x-go-name: MediaMeta
@@ -1530,7 +1648,7 @@ definitions:
Will be null if not set / no action yet taken.
example: Account was suspended.
type: string
- x-go-name: ActionComment
+ x-go-name: ActionTakenComment
category:
description: Under what category was this report created?
example: spam
@@ -3333,6 +3451,147 @@ paths:
summary: Refetch media specified in the database but missing from storage.
tags:
- admin
+ /api/v1/admin/reports:
+ get:
+ description: |-
+ The reports will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
+
+ The next and previous queries can be parsed from the returned Link header.
+
+ Example:
+
+ ```
+ <https://example.org/api/v1/admin/reports?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/reports?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
+ ````
+ operationId: adminReports
+ parameters:
+ - description: If set to true, only resolved reports will be returned. If false, only unresolved reports will be returned. If unset, reports will not be filtered on their resolved status.
+ in: query
+ name: resolved
+ type: boolean
+ - description: Return only reports created by the given account id.
+ in: query
+ name: account_id
+ type: string
+ - description: Return only reports that target the given account id.
+ in: query
+ name: target_account_id
+ type: string
+ - description: Return only reports *OLDER* than the given max ID. The report with the specified ID will not be included in the response.
+ in: query
+ name: max_id
+ type: string
+ - description: Return only reports *NEWER* than the given since ID. The report with the specified ID will not be included in the response. This parameter is functionally equivalent to min_id.
+ in: query
+ name: since_id
+ type: string
+ - description: Return only reports *NEWER* than the given min ID. The report with the specified ID will not be included in the response. This parameter is functionally equivalent to since_id.
+ in: query
+ name: min_id
+ type: string
+ - default: 20
+ description: Number of reports to return. If less than 1, will be clamped to 1. If more than 100, will be clamped to 100.
+ in: query
+ name: limit
+ type: integer
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Array of reports.
+ schema:
+ items:
+ $ref: '#/definitions/adminReport'
+ type: array
+ "400":
+ description: bad request
+ "401":
+ description: unauthorized
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: View user moderation reports.
+ tags:
+ - admin
+ /api/v1/admin/reports/{id}:
+ get:
+ operationId: adminReportGet
+ parameters:
+ - description: The id of the report.
+ in: path
+ name: id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ 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
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: View user moderation report with the given id.
+ tags:
+ - admin
+ /api/v1/admin/reports/{id}/resolve:
+ post:
+ consumes:
+ - application/json
+ - application/xml
+ - multipart/form-data
+ operationId: adminReportResolve
+ parameters:
+ - description: The id of the report.
+ in: path
+ name: id
+ required: true
+ type: string
+ - 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!
+ example: The reported account was suspended.
+ in: formData
+ name: action_taken_comment
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ 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
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: Mark a report as resolved.
+ tags:
+ - admin
/api/v1/apps:
post:
consumes:
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go
@@ -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"
@@ -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 {
@@ -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)
}
diff --git a/internal/api/client/admin/admin_test.go b/internal/api/client/admin/admin_test.go
@@ -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
@@ -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() {
diff --git a/internal/api/client/admin/reportget.go b/internal/api/client/admin/reportget.go
@@ -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)
+}
diff --git a/internal/api/client/admin/reportresolve.go b/internal/api/client/admin/reportresolve.go
@@ -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)
+}
diff --git a/internal/api/client/admin/reportresolve_test.go b/internal/api/client/admin/reportresolve_test.go
@@ -0,0 +1,168 @@
+/*
+ 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_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type ReportResolveTestSuite struct {
+ AdminStandardTestSuite
+}
+
+func (suite *ReportResolveTestSuite) resolveReport(
+ account *gtsmodel.Account,
+ token *gtsmodel.Token,
+ user *gtsmodel.User,
+ targetReportID string,
+ expectedHTTPStatus int,
+ expectedBody string,
+ actionTakenComment *string,
+) (*apimodel.AdminReport, error) {
+ // instantiate recorder + test context
+ recorder := httptest.NewRecorder()
+ ctx, _ := testrig.CreateGinTestContext(recorder, nil)
+ ctx.Set(oauth.SessionAuthorizedAccount, account)
+ ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
+ ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
+ ctx.Set(oauth.SessionAuthorizedUser, user)
+
+ // create the request URI
+ requestPath := admin.ReportsPath + "/" + targetReportID + "/resolve"
+ baseURI := config.GetProtocol() + "://" + config.GetHost()
+ requestURI := baseURI + "/api/" + requestPath
+
+ // create the request
+ ctx.Request = httptest.NewRequest(http.MethodPost, requestURI, nil)
+ ctx.AddParam(admin.IDKey, targetReportID)
+ ctx.Request.Header.Set("accept", "application/json")
+ if actionTakenComment != nil {
+ ctx.Request.Form = url.Values{"action_taken_comment": {*actionTakenComment}}
+ }
+
+ // trigger the handler
+ suite.adminModule.ReportResolvePOSTHandler(ctx)
+
+ // read the response
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ b, err := ioutil.ReadAll(result.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ errs := gtserror.MultiError{}
+
+ if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
+ errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
+ }
+
+ // if we got an expected body, return early
+ if expectedBody != "" {
+ if string(b) != expectedBody {
+ errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
+ }
+ return nil, errs.Combine()
+ }
+
+ resp := &apimodel.AdminReport{}
+ if err := json.Unmarshal(b, &resp); err != nil {
+ return nil, err
+ }
+
+ return resp, nil
+}
+
+func (suite *ReportResolveTestSuite) TestReportResolve1() {
+ testAccount := suite.testAccounts["admin_account"]
+ testToken := suite.testTokens["admin_account"]
+ testUser := suite.testUsers["admin_account"]
+ testReportID := suite.testReports["local_account_2_report_remote_account_1"].ID
+ var actionTakenComment *string = nil
+
+ report, err := suite.resolveReport(testAccount, testToken, testUser, testReportID, http.StatusOK, "", actionTakenComment)
+ suite.NoError(err)
+ suite.NotEmpty(report)
+
+ // report should be resolved
+ suite.True(report.ActionTaken)
+ actionTime, err := util.ParseISO8601(*report.ActionTakenAt)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.WithinDuration(time.Now(), actionTime, 1*time.Minute)
+ updatedTime, err := util.ParseISO8601(report.UpdatedAt)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.WithinDuration(time.Now(), updatedTime, 1*time.Minute)
+ suite.Equal(report.ActionTakenByAccount.ID, testAccount.ID)
+ suite.EqualValues(report.ActionTakenComment, actionTakenComment)
+ suite.EqualValues(report.AssignedAccount.ID, testAccount.ID)
+}
+
+func (suite *ReportResolveTestSuite) TestReportResolve2() {
+ testAccount := suite.testAccounts["admin_account"]
+ testToken := suite.testTokens["admin_account"]
+ testUser := suite.testUsers["admin_account"]
+ testReportID := suite.testReports["local_account_2_report_remote_account_1"].ID
+ var actionTakenComment *string = testrig.StringPtr("no action was taken, this is a frivolous report you boob")
+
+ report, err := suite.resolveReport(testAccount, testToken, testUser, testReportID, http.StatusOK, "", actionTakenComment)
+ suite.NoError(err)
+ suite.NotEmpty(report)
+
+ // report should be resolved
+ suite.True(report.ActionTaken)
+ actionTime, err := util.ParseISO8601(*report.ActionTakenAt)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.WithinDuration(time.Now(), actionTime, 1*time.Minute)
+ updatedTime, err := util.ParseISO8601(report.UpdatedAt)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.WithinDuration(time.Now(), updatedTime, 1*time.Minute)
+ suite.Equal(report.ActionTakenByAccount.ID, testAccount.ID)
+ suite.EqualValues(report.ActionTakenComment, actionTakenComment)
+ suite.EqualValues(report.AssignedAccount.ID, testAccount.ID)
+}
+
+func TestReportResolveTestSuite(t *testing.T) {
+ suite.Run(t, &ReportResolveTestSuite{})
+}
diff --git a/internal/api/client/admin/reportsget.go b/internal/api/client/admin/reportsget.go
@@ -0,0 +1,184 @@
+/*
+ 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 (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "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"
+)
+
+// ReportsGETHandler swagger:operation GET /api/v1/admin/reports adminReports
+//
+// View user moderation reports.
+//
+// The reports will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
+//
+// The next and previous queries can be parsed from the returned Link header.
+//
+// Example:
+//
+// ```
+// <https://example.org/api/v1/admin/reports?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/reports?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
+// ````
+//
+// ---
+// tags:
+// - admin
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: resolved
+// type: boolean
+// description: >-
+// If set to true, only resolved reports will be returned.
+// If false, only unresolved reports will be returned.
+// If unset, reports will not be filtered on their resolved status.
+// in: query
+// -
+// name: account_id
+// type: string
+// description: Return only reports created by the given account id.
+// in: query
+// -
+// name: target_account_id
+// type: string
+// description: Return only reports that target the given account id.
+// in: query
+// -
+// name: max_id
+// type: string
+// description: >-
+// Return only reports *OLDER* than the given max ID.
+// The report with the specified ID will not be included in the response.
+// in: query
+// -
+// name: since_id
+// type: string
+// description: >-
+// Return only reports *NEWER* than the given since ID.
+// The report with the specified ID will not be included in the response.
+// This parameter is functionally equivalent to min_id.
+// in: query
+// -
+// name: min_id
+// type: string
+// description: >-
+// Return only reports *NEWER* than the given min ID.
+// The report with the specified ID will not be included in the response.
+// This parameter is functionally equivalent to since_id.
+// in: query
+// -
+// name: limit
+// type: integer
+// description: >-
+// Number of reports to return.
+// If less than 1, will be clamped to 1.
+// If more than 100, will be clamped to 100.
+// default: 20
+// in: query
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// name: reports
+// description: Array of reports.
+// schema:
+// type: array
+// items:
+// "$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) ReportsGETHandler(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
+ }
+
+ var resolved *bool
+ if resolvedString := c.Query(ResolvedKey); resolvedString != "" {
+ i, err := strconv.ParseBool(resolvedString)
+ if err != nil {
+ err := fmt.Errorf("error parsing %s: %s", ResolvedKey, err)
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+ resolved = &i
+ }
+
+ limit := 20
+ if limitString := c.Query(LimitKey); limitString != "" {
+ i, err := strconv.Atoi(limitString)
+ if err != nil {
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ // normalize
+ if i <= 0 {
+ i = 1
+ } else if i >= 100 {
+ i = 100
+ }
+ limit = i
+ }
+
+ resp, errWithCode := m.processor.AdminReportsGet(c.Request.Context(), authed, resolved, c.Query(AccountIDKey), c.Query(TargetAccountIDKey), c.Query(MaxIDKey), c.Query(SinceIDKey), c.Query(MinIDKey), limit)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
+ return
+ }
+
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
+ c.JSON(http.StatusOK, resp.Items)
+}
diff --git a/internal/api/client/admin/reportsget_test.go b/internal/api/client/admin/reportsget_test.go
@@ -0,0 +1,905 @@
+/*
+ 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_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "strconv"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type ReportsGetTestSuite struct {
+ AdminStandardTestSuite
+}
+
+func (suite *ReportsGetTestSuite) getReports(
+ account *gtsmodel.Account,
+ token *gtsmodel.Token,
+ user *gtsmodel.User,
+ expectedHTTPStatus int,
+ expectedBody string,
+ resolved *bool,
+ accountID string,
+ targetAccountID string,
+ maxID string,
+ sinceID string,
+ minID string,
+ limit int,
+) ([]*apimodel.AdminReport, string, error) {
+ // instantiate recorder + test context
+ recorder := httptest.NewRecorder()
+ ctx, _ := testrig.CreateGinTestContext(recorder, nil)
+ ctx.Set(oauth.SessionAuthorizedAccount, account)
+ ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
+ ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
+ ctx.Set(oauth.SessionAuthorizedUser, user)
+
+ // create the request URI
+ requestPath := admin.ReportsPath + "?" + admin.LimitKey + "=" + strconv.Itoa(limit)
+ if resolved != nil {
+ requestPath = requestPath + "&" + admin.ResolvedKey + "=" + strconv.FormatBool(*resolved)
+ }
+ if accountID != "" {
+ requestPath = requestPath + "&" + admin.AccountIDKey + "=" + accountID
+ }
+ if targetAccountID != "" {
+ requestPath = requestPath + "&" + admin.TargetAccountIDKey + "=" + targetAccountID
+ }
+ if maxID != "" {
+ requestPath = requestPath + "&" + admin.MaxIDKey + "=" + maxID
+ }
+ if sinceID != "" {
+ requestPath = requestPath + "&" + admin.SinceIDKey + "=" + sinceID
+ }
+ if minID != "" {
+ requestPath = requestPath + "&" + admin.MinIDKey + "=" + minID
+ }
+ baseURI := config.GetProtocol() + "://" + config.GetHost()
+ requestURI := baseURI + "/api/" + requestPath
+
+ // create the request
+ ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil)
+ ctx.Request.Header.Set("accept", "application/json")
+
+ // trigger the handler
+ suite.adminModule.ReportsGETHandler(ctx)
+
+ // read the response
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ b, err := ioutil.ReadAll(result.Body)
+ if err != nil {
+ return nil, "", err
+ }
+
+ errs := gtserror.MultiError{}
+
+ if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
+ errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
+ }
+
+ // if we got an expected body, return early
+ if expectedBody != "" {
+ if string(b) != expectedBody {
+ errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
+ }
+ return nil, "", errs.Combine()
+ }
+
+ resp := []*apimodel.AdminReport{}
+ if err := json.Unmarshal(b, &resp); err != nil {
+ return nil, "", err
+ }
+
+ return resp, result.Header.Get("Link"), nil
+}
+
+func (suite *ReportsGetTestSuite) TestReportsGet1() {
+ testAccount := suite.testAccounts["admin_account"]
+ testToken := suite.testTokens["admin_account"]
+ testUser := suite.testUsers["admin_account"]
+
+ reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, "", nil, "", "", "", "", "", 20)
+ suite.NoError(err)
+ suite.NotEmpty(reports)
+
+ b, err := json.MarshalIndent(&reports, "", " ")
+ suite.NoError(err)
+
+ suite.Equal(`[
+ {
+ "id": "01GP3DFY9XQ1TJMZT5BGAZPXX7",
+ "action_taken": true,
+ "action_taken_at": "2022-05-15T15:01:56.000Z",
+ "category": "other",
+ "comment": "this is a turtle, not a person, therefore should not be a poster",
+ "forwarded": true,
+ "created_at": "2022-05-15T14:20:12.000Z",
+ "updated_at": "2022-05-15T14:20:12.000Z",
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "domain": "fossbros-anonymous.io",
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "email": "",
+ "ip": null,
+ "ips": [],
+ "locale": "",
+ "invite_request": null,
+ "role": "user",
+ "confirmed": false,
+ "approved": false,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ }
+ },
+ "target_account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "domain": null,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "email": "tortle.dude@example.org",
+ "ip": "118.44.18.196",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "user",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "acct": "1happyturtle",
+ "display_name": "happy little turtle :3",
+ "locked": true,
+ "bot": false,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "url": "http://localhost:8080/@1happyturtle",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 7,
+ "last_status_at": "2021-10-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "role": "user"
+ },
+ "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
+ },
+ "assigned_account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "domain": null,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "email": "admin@example.org",
+ "ip": "89.122.255.1",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "admin",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "acct": "admin",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "note": "",
+ "url": "http://localhost:8080/@admin",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 4,
+ "last_status_at": "2021-10-20T10:41:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "enable_rss": true,
+ "role": "admin"
+ },
+ "created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
+ },
+ "action_taken_by_account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "domain": null,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "email": "admin@example.org",
+ "ip": "89.122.255.1",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "admin",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "acct": "admin",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "note": "",
+ "url": "http://localhost:8080/@admin",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 4,
+ "last_status_at": "2021-10-20T10:41:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "enable_rss": true,
+ "role": "admin"
+ },
+ "created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
+ },
+ "statuses": [],
+ "rule_ids": [],
+ "action_taken_comment": "user was warned not to be a turtle anymore"
+ },
+ {
+ "id": "01GP3AWY4CRDVRNZKW0TEAMB5R",
+ "action_taken": false,
+ "action_taken_at": null,
+ "category": "other",
+ "comment": "dark souls sucks, please yeet this nerd",
+ "forwarded": true,
+ "created_at": "2022-05-14T10:20:03.000Z",
+ "updated_at": "2022-05-14T10:20:03.000Z",
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "domain": null,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "email": "tortle.dude@example.org",
+ "ip": "118.44.18.196",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "user",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "acct": "1happyturtle",
+ "display_name": "happy little turtle :3",
+ "locked": true,
+ "bot": false,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "url": "http://localhost:8080/@1happyturtle",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 7,
+ "last_status_at": "2021-10-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "role": "user"
+ },
+ "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
+ },
+ "target_account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "domain": "fossbros-anonymous.io",
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "email": "",
+ "ip": null,
+ "ips": [],
+ "locale": "",
+ "invite_request": null,
+ "role": "user",
+ "confirmed": false,
+ "approved": false,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ }
+ },
+ "assigned_account": null,
+ "action_taken_by_account": null,
+ "statuses": [
+ {
+ "id": "01FVW7JHQFSFK166WWKR8CBA6M",
+ "created_at": "2021-09-20T10:40:37.000Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "unlisted",
+ "language": "en",
+ "uri": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "url": "http://fossbros-anonymous.io/@foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "favourited": false,
+ "reblogged": false,
+ "muted": false,
+ "bookmarked": false,
+ "pinned": false,
+ "content": "dark souls status bot: \"thoughts of dog\"",
+ "reblog": null,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [
+ {
+ "id": "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
+ "type": "image",
+ "url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "text_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "preview_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "remote_url": "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpg",
+ "preview_remote_url": "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
+ "meta": {
+ "original": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "small": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "focus": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ "description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
+ "blurhash": "LARysgM_IU_3~pD%M_Rj_39FIAt6"
+ }
+ ],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null
+ }
+ ],
+ "rule_ids": [],
+ "action_taken_comment": null
+ }
+]`, string(b))
+
+ suite.Equal(`<http://localhost:8080/api/v1/admin/reports?limit=20&max_id=01GP3AWY4CRDVRNZKW0TEAMB5R>; rel="next", <http://localhost:8080/api/v1/admin/reports?limit=20&min_id=01GP3DFY9XQ1TJMZT5BGAZPXX7>; rel="prev"`, link)
+}
+
+func (suite *ReportsGetTestSuite) TestReportsGet2() {
+ testAccount := suite.testAccounts["admin_account"]
+ testToken := suite.testTokens["admin_account"]
+ testUser := suite.testUsers["admin_account"]
+ account := suite.testAccounts["local_account_2"]
+
+ reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, "", nil, account.ID, "", "", "", "", 20)
+ suite.NoError(err)
+ suite.NotEmpty(reports)
+
+ b, err := json.MarshalIndent(&reports, "", " ")
+ suite.NoError(err)
+
+ suite.Equal(`[
+ {
+ "id": "01GP3AWY4CRDVRNZKW0TEAMB5R",
+ "action_taken": false,
+ "action_taken_at": null,
+ "category": "other",
+ "comment": "dark souls sucks, please yeet this nerd",
+ "forwarded": true,
+ "created_at": "2022-05-14T10:20:03.000Z",
+ "updated_at": "2022-05-14T10:20:03.000Z",
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "domain": null,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "email": "tortle.dude@example.org",
+ "ip": "118.44.18.196",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "user",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "acct": "1happyturtle",
+ "display_name": "happy little turtle :3",
+ "locked": true,
+ "bot": false,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "url": "http://localhost:8080/@1happyturtle",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 7,
+ "last_status_at": "2021-10-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "role": "user"
+ },
+ "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
+ },
+ "target_account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "domain": "fossbros-anonymous.io",
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "email": "",
+ "ip": null,
+ "ips": [],
+ "locale": "",
+ "invite_request": null,
+ "role": "user",
+ "confirmed": false,
+ "approved": false,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ }
+ },
+ "assigned_account": null,
+ "action_taken_by_account": null,
+ "statuses": [
+ {
+ "id": "01FVW7JHQFSFK166WWKR8CBA6M",
+ "created_at": "2021-09-20T10:40:37.000Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "unlisted",
+ "language": "en",
+ "uri": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "url": "http://fossbros-anonymous.io/@foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "favourited": false,
+ "reblogged": false,
+ "muted": false,
+ "bookmarked": false,
+ "pinned": false,
+ "content": "dark souls status bot: \"thoughts of dog\"",
+ "reblog": null,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [
+ {
+ "id": "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
+ "type": "image",
+ "url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "text_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "preview_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "remote_url": "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpg",
+ "preview_remote_url": "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
+ "meta": {
+ "original": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "small": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "focus": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ "description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
+ "blurhash": "LARysgM_IU_3~pD%M_Rj_39FIAt6"
+ }
+ ],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null
+ }
+ ],
+ "rule_ids": [],
+ "action_taken_comment": null
+ }
+]`, string(b))
+
+ suite.Equal(`<http://localhost:8080/api/v1/admin/reports?limit=20&max_id=01GP3AWY4CRDVRNZKW0TEAMB5R&account_id=01F8MH5NBDF2MV7CTC4Q5128HF>; rel="next", <http://localhost:8080/api/v1/admin/reports?limit=20&min_id=01GP3AWY4CRDVRNZKW0TEAMB5R&account_id=01F8MH5NBDF2MV7CTC4Q5128HF>; rel="prev"`, link)
+}
+
+func (suite *ReportsGetTestSuite) TestReportsGet3() {
+ testAccount := suite.testAccounts["admin_account"]
+ testToken := suite.testTokens["admin_account"]
+ testUser := suite.testUsers["admin_account"]
+ targetAccount := suite.testAccounts["remote_account_1"]
+
+ reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, "", nil, "", targetAccount.ID, "", "", "", 20)
+ suite.NoError(err)
+ suite.NotEmpty(reports)
+
+ b, err := json.MarshalIndent(&reports, "", " ")
+ suite.NoError(err)
+
+ suite.Equal(`[
+ {
+ "id": "01GP3AWY4CRDVRNZKW0TEAMB5R",
+ "action_taken": false,
+ "action_taken_at": null,
+ "category": "other",
+ "comment": "dark souls sucks, please yeet this nerd",
+ "forwarded": true,
+ "created_at": "2022-05-14T10:20:03.000Z",
+ "updated_at": "2022-05-14T10:20:03.000Z",
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "domain": null,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "email": "tortle.dude@example.org",
+ "ip": "118.44.18.196",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "user",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "acct": "1happyturtle",
+ "display_name": "happy little turtle :3",
+ "locked": true,
+ "bot": false,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "url": "http://localhost:8080/@1happyturtle",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 7,
+ "last_status_at": "2021-10-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "role": "user"
+ },
+ "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
+ },
+ "target_account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "domain": "fossbros-anonymous.io",
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "email": "",
+ "ip": null,
+ "ips": [],
+ "locale": "",
+ "invite_request": null,
+ "role": "user",
+ "confirmed": false,
+ "approved": false,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ }
+ },
+ "assigned_account": null,
+ "action_taken_by_account": null,
+ "statuses": [
+ {
+ "id": "01FVW7JHQFSFK166WWKR8CBA6M",
+ "created_at": "2021-09-20T10:40:37.000Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "unlisted",
+ "language": "en",
+ "uri": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "url": "http://fossbros-anonymous.io/@foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "favourited": false,
+ "reblogged": false,
+ "muted": false,
+ "bookmarked": false,
+ "pinned": false,
+ "content": "dark souls status bot: \"thoughts of dog\"",
+ "reblog": null,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [
+ {
+ "id": "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
+ "type": "image",
+ "url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "text_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "preview_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "remote_url": "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpg",
+ "preview_remote_url": "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
+ "meta": {
+ "original": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "small": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "focus": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ "description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
+ "blurhash": "LARysgM_IU_3~pD%M_Rj_39FIAt6"
+ }
+ ],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null
+ }
+ ],
+ "rule_ids": [],
+ "action_taken_comment": null
+ }
+]`, string(b))
+
+ suite.Equal(`<http://localhost:8080/api/v1/admin/reports?limit=20&max_id=01GP3AWY4CRDVRNZKW0TEAMB5R&target_account_id=01F8MH5ZK5VRH73AKHQM6Y9VNX>; rel="next", <http://localhost:8080/api/v1/admin/reports?limit=20&min_id=01GP3AWY4CRDVRNZKW0TEAMB5R&target_account_id=01F8MH5ZK5VRH73AKHQM6Y9VNX>; rel="prev"`, link)
+}
+
+func (suite *ReportsGetTestSuite) TestReportsGet4() {
+ testAccount := suite.testAccounts["admin_account"]
+ testToken := suite.testTokens["admin_account"]
+ testUser := suite.testUsers["admin_account"]
+ resolved := testrig.FalseBool()
+ targetAccount := suite.testAccounts["local_account_2"]
+
+ reports, link, err := suite.getReports(testAccount, testToken, testUser, http.StatusOK, "", resolved, "", targetAccount.ID, "", "", "", 20)
+ suite.NoError(err)
+ suite.Empty(reports)
+
+ b, err := json.MarshalIndent(&reports, "", " ")
+ suite.NoError(err)
+
+ suite.Equal(`[]`, string(b))
+ suite.Empty(link)
+}
+
+func (suite *ReportsGetTestSuite) TestReportsGet6() {
+ testAccount := suite.testAccounts["local_account_1"]
+ testToken := suite.testTokens["local_account_1"]
+ testUser := suite.testUsers["local_account_1"]
+
+ reports, _, err := suite.getReports(testAccount, testToken, testUser, http.StatusForbidden, `{"error":"Forbidden: user 01F8MGVGPHQ2D3P3X0454H54Z5 not an admin"}`, nil, "", "", "", "", "", 20)
+ suite.NoError(err)
+ suite.Empty(reports)
+}
+
+func TestReportsGetTestSuite(t *testing.T) {
+ suite.Run(t, &ReportsGetTestSuite{})
+}
diff --git a/internal/api/model/admin.go b/internal/api/model/admin.go
@@ -19,23 +19,42 @@
package model
// AdminAccountInfo models the admin view of an account's details.
+//
+// swagger:model adminAccountInfo
type AdminAccountInfo struct {
// The ID of the account in the database.
+ // example: 01GQ4PHNT622DQ9X95XQX4KKNR
ID string `json:"id"`
// The username of the account.
+ // example: dril
Username string `json:"username"`
// The domain of the account.
- Domain string `json:"domain"`
+ // Null for local accounts.
+ // example: example.org
+ Domain *string `json:"domain"`
// When the account was first discovered. (ISO 8601 Datetime)
+ // example: 2021-07-30T09:20:25+00:00
CreatedAt string `json:"created_at"`
// The email address associated with the account.
+ // Empty string for remote accounts or accounts with
+ // no known email address.
+ // example: someone@somewhere.com
Email string `json:"email"`
// The IP address last used to login to this account.
- IP string `json:"ip"`
+ // Null if not known.
+ // example: 192.0.2.1
+ IP *string `json:"ip"`
+ // All known IP addresses associated with this account.
+ // NOT IMPLEMENTED (will always be empty array).
+ // example: []
+ IPs []interface{} `json:"ips"`
// The locale of the account. (ISO 639 Part 1 two-letter language code)
+ // example: en
Locale string `json:"locale"`
- // Invite request text
- InviteRequest string `json:"invite_request"`
+ // The reason given when requesting an invite.
+ // Null if not known / remote account.
+ // example: Pleaaaaaaaaaaaaaaase!!
+ InviteRequest *string `json:"invite_request"`
// The current role of the account.
Role string `json:"role"`
// Whether the account has confirmed their email address.
@@ -53,12 +72,67 @@ type AdminAccountInfo struct {
// The ID of the application that created this account.
CreatedByApplicationID string `json:"created_by_application_id,omitempty"`
// The ID of the account that invited this user
- InvitedByAccountID string `json:"invited_by_account_id"`
+ InvitedByAccountID string `json:"invited_by_account_id,omitempty"`
}
-// AdminReportInfo models the admin view of a report.
-type AdminReportInfo struct {
- Report
+// AdminReport models the admin view of a report.
+//
+// swagger:model adminReport
+type AdminReport struct {
+ // ID of the report.
+ // example: 01FBVD42CQ3ZEEVMW180SBX03B
+ ID string `json:"id"`
+ // Whether an action has been taken by an admin in response to this report.
+ // example: false
+ ActionTaken bool `json:"action_taken"`
+ // If an action was taken, at what time was this done? (ISO 8601 Datetime)
+ // Will be null if not set / no action yet taken.
+ // example: 2021-07-30T09:20:25+00:00
+ ActionTakenAt *string `json:"action_taken_at"`
+ // Under what category was this report created?
+ // example: spam
+ Category string `json:"category"`
+ // Comment submitted when the report was created.
+ // Will be empty if no comment was submitted.
+ // example: This person has been harassing me.
+ Comment string `json:"comment"`
+ // Bool to indicate that report should be federated to remote instance.
+ // example: true
+ Forwarded bool `json:"forwarded"`
+ // The date when this report was created (ISO 8601 Datetime).
+ // example: 2021-07-30T09:20:25+00:00
+ CreatedAt string `json:"created_at"`
+ // Time of last action on this report (ISO 8601 Datetime).
+ // example: 2021-07-30T09:20:25+00:00
+ UpdatedAt string `json:"updated_at"`
+ // The account that created the report.
+ Account *AdminAccountInfo `json:"account"`
+ // Account that was reported.
+ TargetAccount *AdminAccountInfo `json:"target_account"`
+ // The account assigned to handle the report.
+ // Null if no account assigned.
+ AssignedAccount *AdminAccountInfo `json:"assigned_account"`
+ // Account that took admin action (if any).
+ // Null if no action (yet) taken.
+ ActionTakenByAccount *AdminAccountInfo `json:"action_taken_by_account"`
+ // Array of statuses that were submitted along with this report.
+ // Will be empty if no status IDs were submitted with the report.
+ Statuses []*Status `json:"statuses"`
+ // Array of rule IDs that were submitted along with this report.
+ // NOT IMPLEMENTED, will always be empty array.
+ Rules []interface{} `json:"rule_ids"`
+ // If an action was taken, what comment was made by the admin on the taken action?
+ // Will be null if not set / no action yet taken.
+ // example: Account was suspended.
+ ActionTakenComment *string `json:"action_taken_comment"`
+}
+
+// AdminReportResolveRequest can be submitted along with a POST to /api/v1/admin/reports/{id}/resolve
+//
+// swagger:ignore
+type AdminReportResolveRequest struct {
+ // Comment to show to the creator of the report when an admin marks it as resolved.
+ ActionTakenComment *string `form:"action_taken_comment" json:"action_taken_comment" xml:"action_taken_comment"`
}
// AdminEmoji models the admin view of a custom emoji.
diff --git a/internal/api/model/report.go b/internal/api/model/report.go
@@ -38,7 +38,7 @@ type Report struct {
// If an action was taken, what comment was made by the admin on the taken action?
// Will be null if not set / no action yet taken.
// example: Account was suspended.
- ActionComment *string `json:"action_taken_comment"`
+ ActionTakenComment *string `json:"action_taken_comment"`
// Under what category was this report created?
// example: spam
Category string `json:"category"`
diff --git a/internal/processing/admin.go b/internal/processing/admin.go
@@ -81,3 +81,15 @@ func (p *processor) AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays in
func (p *processor) AdminMediaRefetch(ctx context.Context, authed *oauth.Auth, domain string) gtserror.WithCode {
return p.adminProcessor.MediaRefetch(ctx, authed.Account, domain)
}
+
+func (p *processor) AdminReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
+ return p.adminProcessor.ReportsGet(ctx, authed.Account, resolved, accountID, targetAccountID, maxID, sinceID, minID, limit)
+}
+
+func (p *processor) AdminReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminReport, gtserror.WithCode) {
+ return p.adminProcessor.ReportGet(ctx, authed.Account, id)
+}
+
+func (p *processor) AdminReportResolve(ctx context.Context, authed *oauth.Auth, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) {
+ return p.adminProcessor.ReportResolve(ctx, authed.Account, id, actionTakenComment)
+}
diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go
@@ -50,6 +50,9 @@ type Processor interface {
EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode)
MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode
MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode
+ ReportsGet(ctx context.Context, account *gtsmodel.Account, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
+ ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode)
+ ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode)
}
type processor struct {
diff --git a/internal/processing/admin/getreport.go b/internal/processing/admin/getreport.go
@@ -0,0 +1,45 @@
+/*
+ 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 (
+ "context"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode) {
+ report, err := p.db.GetReportByID(ctx, id)
+ if err != nil {
+ if err == db.ErrNoEntries {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, report, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apimodelReport, nil
+}
diff --git a/internal/processing/admin/getreports.go b/internal/processing/admin/getreports.go
@@ -0,0 +1,92 @@
+/*
+ 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 (
+ "context"
+ "fmt"
+ "strconv"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) ReportsGet(
+ ctx context.Context,
+ account *gtsmodel.Account,
+ resolved *bool,
+ accountID string,
+ targetAccountID string,
+ maxID string,
+ sinceID string,
+ minID string,
+ limit int,
+) (*apimodel.PageableResponse, gtserror.WithCode) {
+ reports, err := p.db.GetReports(ctx, resolved, accountID, targetAccountID, maxID, sinceID, minID, limit)
+ if err != nil {
+ if err == db.ErrNoEntries {
+ return util.EmptyPageableResponse(), nil
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ count := len(reports)
+ items := make([]interface{}, 0, count)
+ nextMaxIDValue := ""
+ prevMinIDValue := ""
+ for i, r := range reports {
+ item, err := p.tc.ReportToAdminAPIReport(ctx, r, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err))
+ }
+
+ if i == count-1 {
+ nextMaxIDValue = item.ID
+ }
+
+ if i == 0 {
+ prevMinIDValue = item.ID
+ }
+
+ items = append(items, item)
+ }
+
+ extraQueryParams := []string{}
+ if resolved != nil {
+ extraQueryParams = append(extraQueryParams, "resolved="+strconv.FormatBool(*resolved))
+ }
+ if accountID != "" {
+ extraQueryParams = append(extraQueryParams, "account_id="+accountID)
+ }
+ if targetAccountID != "" {
+ extraQueryParams = append(extraQueryParams, "target_account_id="+targetAccountID)
+ }
+
+ return util.PackagePageableResponse(util.PageableResponseParams{
+ Items: items,
+ Path: "/api/v1/admin/reports",
+ NextMaxIDValue: nextMaxIDValue,
+ PrevMinIDValue: prevMinIDValue,
+ Limit: limit,
+ ExtraQueryParams: extraQueryParams,
+ })
+}
diff --git a/internal/processing/admin/resolvereport.go b/internal/processing/admin/resolvereport.go
@@ -0,0 +1,64 @@
+/*
+ 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 (
+ "context"
+ "time"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) {
+ report, err := p.db.GetReportByID(ctx, id)
+ if err != nil {
+ if err == db.ErrNoEntries {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ columns := []string{
+ "action_taken_at",
+ "action_taken_by_account_id",
+ }
+
+ report.ActionTakenAt = time.Now()
+ report.ActionTakenByAccountID = account.ID
+
+ if actionTakenComment != nil {
+ report.ActionTaken = *actionTakenComment
+ columns = append(columns, "action_taken")
+ }
+
+ updatedReport, err := p.db.UpdateReport(ctx, report, columns...)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, updatedReport, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apimodelReport, nil
+}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
@@ -140,6 +140,13 @@ type Processor interface {
AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode
// AdminMediaRefetch triggers a refetch of remote media for the given domain (or all if domain is empty).
AdminMediaRefetch(ctx context.Context, authed *oauth.Auth, domain string) gtserror.WithCode
+ // AdminReportsGet returns a list of user moderation reports.
+ AdminReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
+ // AdminReportGet returns a single user moderation report, specified by id.
+ AdminReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminReport, gtserror.WithCode)
+ // AdminReportResolve marks a single user moderation report as resolved, with the given id.
+ // actionTakenComment is optional: if set, this will be stored as a comment on the action taken.
+ AdminReportResolve(ctx context.Context, authed *oauth.Auth, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode)
// AppCreate processes the creation of a new API application
AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode)
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
@@ -89,6 +89,8 @@ type TypeConverter interface {
DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error)
// ReportToAPIReport converts a gts model report into an api model report, for serving at /api/v1/reports
ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (*apimodel.Report, error)
+ // ReportToAdminAPIReport converts a gts model report into an admin view report, for serving at /api/v1/admin/reports
+ ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Report, requestingAccount *gtsmodel.Account) (*apimodel.AdminReport, error)
/*
INTERNAL (gts) MODEL TO FRONTEND (rss) MODEL
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
@@ -256,6 +256,83 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
}, nil
}
+func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Account) (*apimodel.AdminAccountInfo, error) {
+ var (
+ email string
+ ip *string
+ domain *string
+ locale string
+ confirmed bool
+ inviteRequest *string
+ approved bool
+ disabled bool
+ silenced bool
+ suspended bool
+ role apimodel.AccountRole = apimodel.AccountRoleUser // assume user by default
+ createdByApplicationID string
+ )
+
+ // take user-level information if possible
+ if a.Domain != "" {
+ domain = &a.Domain
+ } else {
+ user, err := c.db.GetUserByAccountID(ctx, a.ID)
+ if err != nil {
+ return nil, fmt.Errorf("AccountToAdminAPIAccount: error getting user from database for account id %s: %w", a.ID, err)
+ }
+
+ if user.Email != "" {
+ email = user.Email
+ } else {
+ email = user.UnconfirmedEmail
+ }
+
+ if i := user.CurrentSignInIP.String(); i != "<nil>" {
+ ip = &i
+ }
+
+ locale = user.Locale
+ inviteRequest = &user.Account.Reason
+ if *user.Admin {
+ role = apimodel.AccountRoleAdmin
+ } else if *user.Moderator {
+ role = apimodel.AccountRoleModerator
+ }
+ confirmed = !user.ConfirmedAt.IsZero()
+ approved = *user.Approved
+ disabled = *user.Disabled
+ silenced = !user.Account.SilencedAt.IsZero()
+ suspended = !user.Account.SuspendedAt.IsZero()
+ createdByApplicationID = user.CreatedByApplicationID
+ }
+
+ apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
+ if err != nil {
+ return nil, fmt.Errorf("AccountToAdminAPIAccount: error converting account to api account for account id %s: %w", a.ID, err)
+ }
+
+ return &apimodel.AdminAccountInfo{
+ ID: a.ID,
+ Username: a.Username,
+ Domain: domain,
+ CreatedAt: util.FormatISO8601(a.CreatedAt),
+ Email: email,
+ IP: ip,
+ IPs: []interface{}{}, // not implemented,
+ Locale: locale,
+ InviteRequest: inviteRequest,
+ Role: string(role),
+ Confirmed: confirmed,
+ Approved: approved,
+ Disabled: disabled,
+ Silenced: silenced,
+ Suspended: suspended,
+ Account: apiAccount,
+ CreatedByApplicationID: createdByApplicationID,
+ InvitedByAccountID: "", // not implemented (yet)
+ }, nil
+}
+
func (c *converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Application) (*apimodel.Application, error) {
return &apimodel.Application{
ID: a.ID,
@@ -825,7 +902,7 @@ func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (
}
if actionComment := r.ActionTaken; actionComment != "" {
- report.ActionComment = &actionComment
+ report.ActionTakenComment = &actionComment
}
if r.TargetAccount == nil {
@@ -845,6 +922,93 @@ func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (
return report, nil
}
+func (c *converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Report, requestingAccount *gtsmodel.Account) (*apimodel.AdminReport, error) {
+ var (
+ err error
+ actionTakenAt *string
+ actionTakenComment *string
+ actionTakenByAccount *apimodel.AdminAccountInfo
+ )
+
+ if !r.ActionTakenAt.IsZero() {
+ ata := util.FormatISO8601(r.ActionTakenAt)
+ actionTakenAt = &ata
+ }
+
+ if r.Account == nil {
+ r.Account, err = c.db.GetAccountByID(ctx, r.AccountID)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error getting account with id %s from the db: %w", r.AccountID, err)
+ }
+ }
+ account, err := c.AccountToAdminAPIAccount(ctx, r.Account)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error converting account with id %s to adminAPIAccount: %w", r.AccountID, err)
+ }
+
+ if r.TargetAccount == nil {
+ r.TargetAccount, err = c.db.GetAccountByID(ctx, r.TargetAccountID)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error getting target account with id %s from the db: %w", r.TargetAccountID, err)
+ }
+ }
+ targetAccount, err := c.AccountToAdminAPIAccount(ctx, r.TargetAccount)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error converting target account with id %s to adminAPIAccount: %w", r.TargetAccountID, err)
+ }
+
+ if r.ActionTakenByAccountID != "" {
+ if r.ActionTakenByAccount == nil {
+ r.ActionTakenByAccount, err = c.db.GetAccountByID(ctx, r.ActionTakenByAccountID)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error getting action taken by account with id %s from the db: %w", r.ActionTakenByAccountID, err)
+ }
+ }
+
+ actionTakenByAccount, err = c.AccountToAdminAPIAccount(ctx, r.ActionTakenByAccount)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error converting action taken by account with id %s to adminAPIAccount: %w", r.ActionTakenByAccountID, err)
+ }
+ }
+
+ statuses := make([]*apimodel.Status, 0, len(r.StatusIDs))
+ if len(r.StatusIDs) != 0 && len(r.Statuses) == 0 {
+ r.Statuses, err = c.db.GetStatuses(ctx, r.StatusIDs)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error getting statuses from the db: %w", err)
+ }
+ }
+ for _, s := range r.Statuses {
+ status, err := c.StatusToAPIStatus(ctx, s, requestingAccount)
+ if err != nil {
+ return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
+ }
+ statuses = append(statuses, status)
+ }
+
+ if ac := r.ActionTaken; ac != "" {
+ actionTakenComment = &ac
+ }
+
+ return &apimodel.AdminReport{
+ ID: r.ID,
+ ActionTaken: !r.ActionTakenAt.IsZero(),
+ ActionTakenAt: actionTakenAt,
+ Category: "other", // todo: only support default 'other' category right now
+ Comment: r.Comment,
+ Forwarded: *r.Forwarded,
+ CreatedAt: util.FormatISO8601(r.CreatedAt),
+ UpdatedAt: util.FormatISO8601(r.UpdatedAt),
+ Account: account,
+ TargetAccount: targetAccount,
+ AssignedAccount: actionTakenByAccount,
+ ActionTakenByAccount: actionTakenByAccount,
+ ActionTakenComment: actionTakenComment,
+ Statuses: statuses,
+ Rules: []interface{}{}, // not implemented
+ }, nil
+}
+
// convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied.
func (c *converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]apimodel.Attachment, error) {
var errs gtserror.MultiError
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
@@ -691,6 +691,372 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() {
}`, string(b))
}
+func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
+ requestingAccount := suite.testAccounts["admin_account"]
+ adminReport, err := suite.typeconverter.ReportToAdminAPIReport(context.Background(), suite.testReports["remote_account_1_report_local_account_2"], requestingAccount)
+ suite.NoError(err)
+
+ b, err := json.MarshalIndent(adminReport, "", " ")
+ suite.NoError(err)
+
+ suite.Equal(`{
+ "id": "01GP3DFY9XQ1TJMZT5BGAZPXX7",
+ "action_taken": true,
+ "action_taken_at": "2022-05-15T15:01:56.000Z",
+ "category": "other",
+ "comment": "this is a turtle, not a person, therefore should not be a poster",
+ "forwarded": true,
+ "created_at": "2022-05-15T14:20:12.000Z",
+ "updated_at": "2022-05-15T14:20:12.000Z",
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "domain": "fossbros-anonymous.io",
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "email": "",
+ "ip": null,
+ "ips": [],
+ "locale": "",
+ "invite_request": null,
+ "role": "user",
+ "confirmed": false,
+ "approved": false,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ }
+ },
+ "target_account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "domain": null,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "email": "tortle.dude@example.org",
+ "ip": "118.44.18.196",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "user",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "acct": "1happyturtle",
+ "display_name": "happy little turtle :3",
+ "locked": true,
+ "bot": false,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "url": "http://localhost:8080/@1happyturtle",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 7,
+ "last_status_at": "2021-10-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "role": "user"
+ },
+ "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
+ },
+ "assigned_account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "domain": null,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "email": "admin@example.org",
+ "ip": "89.122.255.1",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "admin",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "acct": "admin",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "note": "",
+ "url": "http://localhost:8080/@admin",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 4,
+ "last_status_at": "2021-10-20T10:41:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "enable_rss": true,
+ "role": "admin"
+ },
+ "created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
+ },
+ "action_taken_by_account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "domain": null,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "email": "admin@example.org",
+ "ip": "89.122.255.1",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "admin",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH17FWEB39HZJ76B6VXSKF",
+ "username": "admin",
+ "acct": "admin",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "created_at": "2022-05-17T13:10:59.000Z",
+ "note": "",
+ "url": "http://localhost:8080/@admin",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 4,
+ "last_status_at": "2021-10-20T10:41:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "enable_rss": true,
+ "role": "admin"
+ },
+ "created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
+ },
+ "statuses": [],
+ "rule_ids": [],
+ "action_taken_comment": "user was warned not to be a turtle anymore"
+}`, string(b))
+}
+
+func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
+ requestingAccount := suite.testAccounts["admin_account"]
+ adminReport, err := suite.typeconverter.ReportToAdminAPIReport(context.Background(), suite.testReports["local_account_2_report_remote_account_1"], requestingAccount)
+ suite.NoError(err)
+
+ b, err := json.MarshalIndent(adminReport, "", " ")
+ suite.NoError(err)
+
+ suite.Equal(`{
+ "id": "01GP3AWY4CRDVRNZKW0TEAMB5R",
+ "action_taken": false,
+ "action_taken_at": null,
+ "category": "other",
+ "comment": "dark souls sucks, please yeet this nerd",
+ "forwarded": true,
+ "created_at": "2022-05-14T10:20:03.000Z",
+ "updated_at": "2022-05-14T10:20:03.000Z",
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "domain": null,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "email": "tortle.dude@example.org",
+ "ip": "118.44.18.196",
+ "ips": [],
+ "locale": "en",
+ "invite_request": "",
+ "role": "user",
+ "confirmed": true,
+ "approved": true,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "username": "1happyturtle",
+ "acct": "1happyturtle",
+ "display_name": "happy little turtle :3",
+ "locked": true,
+ "bot": false,
+ "created_at": "2022-06-04T13:12:00.000Z",
+ "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "url": "http://localhost:8080/@1happyturtle",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 7,
+ "last_status_at": "2021-10-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": [],
+ "role": "user"
+ },
+ "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
+ },
+ "target_account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "domain": "fossbros-anonymous.io",
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "email": "",
+ "ip": null,
+ "ips": [],
+ "locale": "",
+ "invite_request": null,
+ "role": "user",
+ "confirmed": false,
+ "approved": false,
+ "disabled": false,
+ "silenced": false,
+ "suspended": false,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ }
+ },
+ "assigned_account": null,
+ "action_taken_by_account": null,
+ "statuses": [
+ {
+ "id": "01FVW7JHQFSFK166WWKR8CBA6M",
+ "created_at": "2021-09-20T10:40:37.000Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "unlisted",
+ "language": "en",
+ "uri": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "url": "http://fossbros-anonymous.io/@foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "favourited": false,
+ "reblogged": false,
+ "muted": false,
+ "bookmarked": false,
+ "pinned": false,
+ "content": "dark souls status bot: \"thoughts of dog\"",
+ "reblog": null,
+ "account": {
+ "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
+ "username": "foss_satan",
+ "acct": "foss_satan@fossbros-anonymous.io",
+ "display_name": "big gerald",
+ "locked": false,
+ "bot": false,
+ "created_at": "2021-09-26T10:52:36.000Z",
+ "note": "i post about like, i dunno, stuff, or whatever!!!!",
+ "url": "http://fossbros-anonymous.io/@foss_satan",
+ "avatar": "",
+ "avatar_static": "",
+ "header": "http://localhost:8080/assets/default_header.png",
+ "header_static": "http://localhost:8080/assets/default_header.png",
+ "followers_count": 0,
+ "following_count": 0,
+ "statuses_count": 1,
+ "last_status_at": "2021-09-20T10:40:37.000Z",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [
+ {
+ "id": "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
+ "type": "image",
+ "url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "text_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "preview_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
+ "remote_url": "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpg",
+ "preview_remote_url": "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
+ "meta": {
+ "original": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "small": {
+ "width": 472,
+ "height": 291,
+ "size": "472x291",
+ "aspect": 1.6219932
+ },
+ "focus": {
+ "x": 0,
+ "y": 0
+ }
+ },
+ "description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
+ "blurhash": "LARysgM_IU_3~pD%M_Rj_39FIAt6"
+ }
+ ],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null
+ }
+ ],
+ "rule_ids": [],
+ "action_taken_comment": null
+}`, string(b))
+}
+
func TestInternalToFrontendTestSuite(t *testing.T) {
suite.Run(t, new(InternalToFrontendTestSuite))
}
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
@@ -1947,7 +1947,7 @@ func NewTestReports() map[string]*gtsmodel.Report {
Forwarded: TrueBool(),
ActionTaken: "user was warned not to be a turtle anymore",
ActionTakenAt: TimeMustParse("2022-05-15T17:01:56+02:00"),
- ActionTakenByAccountID: "01AY6P665V14JJR0AFVRT7311Y",
+ ActionTakenByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
},
}
}