[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
This commit is contained in:
tobi 2023-01-25 11:12:17 +01:00 committed by GitHub
commit faeb7ded3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 2674 additions and 72 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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