From 8d07d85e2aa9cf07e8131946e36c953e4f17438f Mon Sep 17 00:00:00 2001 From: tobi Date: Sun, 16 Jun 2024 14:37:45 +0200 Subject: [PATCH 1/8] use apiutil + paging in admin processor+handlers --- docs/api/swagger.yaml | 22 ++++--- internal/api/client/admin/accountaction.go | 7 +-- internal/api/client/admin/admin.go | 25 +++----- internal/api/client/admin/emojidelete.go | 8 +-- internal/api/client/admin/emojidelete_test.go | 7 ++- internal/api/client/admin/emojiget.go | 8 +-- internal/api/client/admin/emojiget_test.go | 7 ++- internal/api/client/admin/emojisget.go | 20 ++---- internal/api/client/admin/emojiupdate.go | 7 +-- internal/api/client/admin/emojiupdate_test.go | 23 +++---- internal/api/client/admin/reportget.go | 8 +-- internal/api/client/admin/reportresolve.go | 8 +-- .../api/client/admin/reportresolve_test.go | 3 +- internal/api/client/admin/reportsget.go | 58 ++++++++---------- internal/api/client/admin/reportsget_test.go | 21 ++++--- internal/api/client/admin/ruledelete.go | 8 +-- internal/api/client/admin/ruleget.go | 8 +-- internal/api/client/admin/ruleupdate.go | 8 +-- internal/api/client/reports/reportget.go | 8 +-- internal/api/client/reports/reportget_test.go | 2 +- internal/api/client/reports/reports.go | 12 +--- internal/api/client/reports/reportsget.go | 61 ++++++++----------- .../api/client/reports/reportsget_test.go | 13 ++-- internal/api/client/search/searchget.go | 2 +- internal/api/client/search/searchget_test.go | 2 +- internal/api/util/parsequery.go | 35 ++++++++--- internal/db/bundb/report.go | 44 ++++++++++--- internal/db/bundb/report_test.go | 17 +++++- internal/db/report.go | 3 +- internal/processing/admin/report.go | 60 ++++++++++-------- internal/processing/report/get.go | 56 ++++++++++------- 31 files changed, 303 insertions(+), 268 deletions(-) diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 46ed95c827..2e79bc96be 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -4525,6 +4525,8 @@ paths: - default: 50 description: Number of emojis to return. Less than 1, or not set, means unlimited (all emojis). in: query + maximum: 200 + minimum: 0 name: limit type: integer - description: |- @@ -5739,21 +5741,23 @@ paths: 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. + - description: Return only reports *OLDER* than the given max ID (for paging downwards). 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. + - description: Return only reports *NEWER* than the given since 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 min ID. The report with the specified ID will not be included in the response. This parameter is functionally equivalent to since_id. + - description: Return only reports immediately *NEWER* than the given min ID (for paging upwards). The report with the specified ID will not be included in the response. in: query name: min_id type: string - default: 20 - description: Number of reports to return. If more than 100 or less than 1, will be clamped to 100. + description: Number of reports to return. in: query + maximum: 100 + minimum: 1 name: limit type: integer produces: @@ -7682,21 +7686,23 @@ paths: 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. + - description: Return only reports *OLDER* than the given max ID (for paging downwards). 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. + - description: Return only reports *NEWER* than the given since 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 min ID. The report with the specified ID will not be included in the response. This parameter is functionally equivalent to since_id. + - description: Return only reports immediately *NEWER* than the given min ID (for paging upwards). The report with the specified ID will not be included in the response. 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. + description: Number of reports to return. in: query + maximum: 100 + minimum: 1 name: limit type: integer produces: diff --git a/internal/api/client/admin/accountaction.go b/internal/api/client/admin/accountaction.go index 7d74e85301..64e6c39cad 100644 --- a/internal/api/client/admin/accountaction.go +++ b/internal/api/client/admin/accountaction.go @@ -116,10 +116,9 @@ func (m *Module) AccountActionPOSTHandler(c *gin.Context) { return } - targetAcctID := c.Param(IDKey) - if targetAcctID == "" { - err := errors.New("no account id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + targetAcctID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } form.TargetID = targetAcctID diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go index ef54f0b50c..2c55de2f0f 100644 --- a/internal/api/client/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -22,6 +22,7 @@ import ( "codeberg.org/gruf/go-debug" "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/state" ) @@ -29,48 +30,40 @@ import ( const ( BasePath = "/v1/admin" EmojiPath = BasePath + "/custom_emojis" - EmojiPathWithID = EmojiPath + "/:" + IDKey + EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey EmojiCategoriesPath = EmojiPath + "/categories" DomainBlocksPath = BasePath + "/domain_blocks" - DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey + DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey DomainAllowsPath = BasePath + "/domain_allows" - DomainAllowsPathWithID = DomainAllowsPath + "/:" + IDKey + DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey DomainKeysExpirePath = BasePath + "/domain_keys_expire" HeaderAllowsPath = BasePath + "/header_allows" - HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + IDKey + HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey HeaderBlocksPath = BasePath + "/header_blocks" - HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + IDKey + HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey AccountsV1Path = BasePath + "/accounts" AccountsV2Path = "/v2/admin/accounts" - AccountsPathWithID = AccountsV1Path + "/:" + IDKey + AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey AccountsActionPath = AccountsPathWithID + "/action" AccountsApprovePath = AccountsPathWithID + "/approve" AccountsRejectPath = AccountsPathWithID + "/reject" MediaCleanupPath = BasePath + "/media_cleanup" MediaRefetchPath = BasePath + "/media_refetch" ReportsPath = BasePath + "/reports" - ReportsPathWithID = ReportsPath + "/:" + IDKey + ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey ReportsResolvePath = ReportsPathWithID + "/resolve" EmailPath = BasePath + "/email" EmailTestPath = EmailPath + "/test" InstanceRulesPath = BasePath + "/instance/rules" - InstanceRulesPathWithID = InstanceRulesPath + "/:" + IDKey + InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey DebugPath = BasePath + "/debug" DebugAPUrlPath = DebugPath + "/apurl" DebugClearCachesPath = DebugPath + "/caches/clear" - IDKey = "id" FilterQueryKey = "filter" MaxShortcodeDomainKey = "max_shortcode_domain" MinShortcodeDomainKey = "min_shortcode_domain" - LimitKey = "limit" DomainQueryKey = "domain" - ResolvedKey = "resolved" - AccountIDKey = "account_id" - TargetAccountIDKey = "target_account_id" - MaxIDKey = "max_id" - SinceIDKey = "since_id" - MinIDKey = "min_id" ) type Module struct { diff --git a/internal/api/client/admin/emojidelete.go b/internal/api/client/admin/emojidelete.go index 47248a1b9b..9f9f9d2865 100644 --- a/internal/api/client/admin/emojidelete.go +++ b/internal/api/client/admin/emojidelete.go @@ -18,7 +18,6 @@ package admin import ( - "errors" "fmt" "net/http" @@ -97,10 +96,9 @@ func (m *Module) EmojiDELETEHandler(c *gin.Context) { return } - emojiID := c.Param(IDKey) - if emojiID == "" { - err := errors.New("no emoji id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + emojiID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/emojidelete_test.go b/internal/api/client/admin/emojidelete_test.go index e27592baf6..10cf3fe8d4 100644 --- a/internal/api/client/admin/emojidelete_test.go +++ b/internal/api/client/admin/emojidelete_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/db" ) @@ -41,7 +42,7 @@ func (suite *EmojiDeleteTestSuite) TestEmojiDelete1() { path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodDelete, nil, path, "application/json") - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) suite.adminModule.EmojiDELETEHandler(ctx) suite.Equal(http.StatusOK, recorder.Code) @@ -78,7 +79,7 @@ func (suite *EmojiDeleteTestSuite) TestEmojiDelete2() { path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodDelete, nil, path, "application/json") - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) suite.adminModule.EmojiDELETEHandler(ctx) suite.Equal(http.StatusBadRequest, recorder.Code) @@ -100,7 +101,7 @@ func (suite *EmojiDeleteTestSuite) TestEmojiDeleteNotFound() { path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodDelete, nil, path, "application/json") - ctx.AddParam(admin.IDKey, "01GF8VRXX1R00X7XH8973Z29R1") + ctx.AddParam(apiutil.IDKey, "01GF8VRXX1R00X7XH8973Z29R1") suite.adminModule.EmojiDELETEHandler(ctx) suite.Equal(http.StatusNotFound, recorder.Code) diff --git a/internal/api/client/admin/emojiget.go b/internal/api/client/admin/emojiget.go index 730d92db1f..7ecbcfa19f 100644 --- a/internal/api/client/admin/emojiget.go +++ b/internal/api/client/admin/emojiget.go @@ -18,7 +18,6 @@ package admin import ( - "errors" "fmt" "net/http" @@ -82,10 +81,9 @@ func (m *Module) EmojiGETHandler(c *gin.Context) { return } - emojiID := c.Param(IDKey) - if emojiID == "" { - err := errors.New("no emoji id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + emojiID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/emojiget_test.go b/internal/api/client/admin/emojiget_test.go index 86c847b17f..b8bad2536c 100644 --- a/internal/api/client/admin/emojiget_test.go +++ b/internal/api/client/admin/emojiget_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" ) type EmojiGetTestSuite struct { @@ -39,7 +40,7 @@ func (suite *EmojiGetTestSuite) TestEmojiGet1() { path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) suite.adminModule.EmojiGETHandler(ctx) suite.Equal(http.StatusOK, recorder.Code) @@ -71,7 +72,7 @@ func (suite *EmojiGetTestSuite) TestEmojiGet2() { path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) suite.adminModule.EmojiGETHandler(ctx) suite.Equal(http.StatusOK, recorder.Code) @@ -102,7 +103,7 @@ func (suite *EmojiGetTestSuite) TestEmojiGetNotFound() { path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") - ctx.AddParam(admin.IDKey, "01GF8VRXX1R00X7XH8973Z29R1") + ctx.AddParam(apiutil.IDKey, "01GF8VRXX1R00X7XH8973Z29R1") suite.adminModule.EmojiGETHandler(ctx) suite.Equal(http.StatusNotFound, recorder.Code) diff --git a/internal/api/client/admin/emojisget.go b/internal/api/client/admin/emojisget.go index 4013e18365..d50b553acd 100644 --- a/internal/api/client/admin/emojisget.go +++ b/internal/api/client/admin/emojisget.go @@ -20,7 +20,6 @@ package admin import ( "fmt" "net/http" - "strconv" "strings" "github.com/gin-gonic/gin" @@ -76,6 +75,8 @@ import ( // type: integer // description: Number of emojis to return. Less than 1, or not set, means unlimited (all emojis). // default: 50 +// minimum: 0 +// maximum: 200 // in: query // - // name: max_shortcode_domain @@ -142,19 +143,10 @@ func (m *Module) EmojisGETHandler(c *gin.Context) { maxShortcodeDomain := c.Query(MaxShortcodeDomainKey) minShortcodeDomain := c.Query(MinShortcodeDomainKey) - limit := 50 - limitString := c.Query(LimitKey) - if limitString != "" { - i, err := strconv.ParseInt(limitString, 10, 32) - if err != nil { - err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) - return - } - limit = int(i) - } - if limit < 0 { - limit = 0 + limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 50, 200, 0) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return } var domain string diff --git a/internal/api/client/admin/emojiupdate.go b/internal/api/client/admin/emojiupdate.go index 1d41dd545a..37f67cabd4 100644 --- a/internal/api/client/admin/emojiupdate.go +++ b/internal/api/client/admin/emojiupdate.go @@ -147,10 +147,9 @@ func (m *Module) EmojiPATCHHandler(c *gin.Context) { return } - emojiID := c.Param(IDKey) - if emojiID == "" { - err := errors.New("no emoji id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + emojiID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/emojiupdate_test.go b/internal/api/client/admin/emojiupdate_test.go index eb9d9d866a..676363e391 100644 --- a/internal/api/client/admin/emojiupdate_test.go +++ b/internal/api/client/admin/emojiupdate_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -53,7 +54,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateNewCategory() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -130,7 +131,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateSwitchCategory() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -208,7 +209,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyRemoteToLocal() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -284,7 +285,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateDisableEmoji() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -325,7 +326,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateDisableLocalEmoji() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -358,7 +359,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyRemoteEmoji() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -391,7 +392,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -425,7 +426,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyLocalToLocal() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -459,7 +460,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -492,7 +493,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyNoShortcode() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) @@ -526,7 +527,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyShortcodeAlreadyInUse() { bodyBytes := requestBody.Bytes() recorder := httptest.NewRecorder() ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPathWithID, w.FormDataContentType()) - ctx.AddParam(admin.IDKey, testEmoji.ID) + ctx.AddParam(apiutil.IDKey, testEmoji.ID) // call the handler suite.adminModule.EmojiPATCHHandler(ctx) diff --git a/internal/api/client/admin/reportget.go b/internal/api/client/admin/reportget.go index f70ae8b549..f2acd214c7 100644 --- a/internal/api/client/admin/reportget.go +++ b/internal/api/client/admin/reportget.go @@ -18,7 +18,6 @@ package admin import ( - "errors" "fmt" "net/http" @@ -85,10 +84,9 @@ func (m *Module) ReportGETHandler(c *gin.Context) { return } - reportID := c.Param(IDKey) - if reportID == "" { - err := errors.New("no report id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + reportID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/reportresolve.go b/internal/api/client/admin/reportresolve.go index 51c268a2d3..f17ae24bed 100644 --- a/internal/api/client/admin/reportresolve.go +++ b/internal/api/client/admin/reportresolve.go @@ -18,7 +18,6 @@ package admin import ( - "errors" "fmt" "net/http" @@ -107,10 +106,9 @@ func (m *Module) ReportResolvePOSTHandler(c *gin.Context) { return } - reportID := c.Param(IDKey) - if reportID == "" { - err := errors.New("no report id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + reportID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/reportresolve_test.go b/internal/api/client/admin/reportresolve_test.go index 087080a705..561661fe03 100644 --- a/internal/api/client/admin/reportresolve_test.go +++ b/internal/api/client/admin/reportresolve_test.go @@ -29,6 +29,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -65,7 +66,7 @@ func (suite *ReportResolveTestSuite) resolveReport( // create the request ctx.Request = httptest.NewRequest(http.MethodPost, requestURI, nil) - ctx.AddParam(admin.IDKey, targetReportID) + ctx.AddParam(apiutil.IDKey, targetReportID) ctx.Request.Header.Set("accept", "application/json") if actionTakenComment != nil { ctx.Request.Form = url.Values{"action_taken_comment": {*actionTakenComment}} diff --git a/internal/api/client/admin/reportsget.go b/internal/api/client/admin/reportsget.go index 58501a6d78..893960e2ad 100644 --- a/internal/api/client/admin/reportsget.go +++ b/internal/api/client/admin/reportsget.go @@ -20,12 +20,12 @@ 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" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) // ReportsGETHandler swagger:operation GET /api/v1/admin/reports adminReports @@ -72,7 +72,7 @@ import ( // name: max_id // type: string // description: >- -// Return only reports *OLDER* than the given max ID. +// Return only reports *OLDER* than the given max ID (for paging downwards). // The report with the specified ID will not be included in the response. // in: query // - @@ -81,23 +81,21 @@ import ( // 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. +// Return only reports immediately *NEWER* than the given min ID (for paging upwards). // 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 more than 100 or less than 1, will be clamped to 100. +// description: Number of reports to return. // default: 20 +// minimum: 1 +// maximum: 100 // in: query // // security: @@ -144,34 +142,30 @@ func (m *Module) ReportsGETHandler(c *gin.Context) { 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.InstanceGetV1) - return - } - resolved = &i + resolved, errWithCode := apiutil.ParseResolved(c.Query(apiutil.ResolvedKey), nil) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return } - 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.InstanceGetV1) - return - } - - // normalize - if i < 1 || i > 100 { - i = 100 - } - limit = i + page, errWithCode := paging.ParseIDPage(c, + 1, // min limit + 100, // max limit + 20, // default limit + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return } - resp, errWithCode := m.processor.Admin().ReportsGet(c.Request.Context(), authed.Account, resolved, c.Query(AccountIDKey), c.Query(TargetAccountIDKey), c.Query(MaxIDKey), c.Query(SinceIDKey), c.Query(MinIDKey), limit) + resp, errWithCode := m.processor.Admin().ReportsGet( + c.Request.Context(), + authed.Account, + resolved, + c.Query(apiutil.AccountIDKey), + c.Query(apiutil.TargetAccountIDKey), + page, + ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/reportsget_test.go b/internal/api/client/admin/reportsget_test.go index b20921b360..28efabc00b 100644 --- a/internal/api/client/admin/reportsget_test.go +++ b/internal/api/client/admin/reportsget_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -63,24 +64,24 @@ func (suite *ReportsGetTestSuite) getReports( ctx.Set(oauth.SessionAuthorizedUser, user) // create the request URI - requestPath := admin.ReportsPath + "?" + admin.LimitKey + "=" + strconv.Itoa(limit) + requestPath := admin.ReportsPath + "?" + apiutil.LimitKey + "=" + strconv.Itoa(limit) if resolved != nil { - requestPath = requestPath + "&" + admin.ResolvedKey + "=" + strconv.FormatBool(*resolved) + requestPath = requestPath + "&" + apiutil.ResolvedKey + "=" + strconv.FormatBool(*resolved) } if accountID != "" { - requestPath = requestPath + "&" + admin.AccountIDKey + "=" + accountID + requestPath = requestPath + "&" + apiutil.AccountIDKey + "=" + accountID } if targetAccountID != "" { - requestPath = requestPath + "&" + admin.TargetAccountIDKey + "=" + targetAccountID + requestPath = requestPath + "&" + apiutil.TargetAccountIDKey + "=" + targetAccountID } if maxID != "" { - requestPath = requestPath + "&" + admin.MaxIDKey + "=" + maxID + requestPath = requestPath + "&" + apiutil.MaxIDKey + "=" + maxID } if sinceID != "" { - requestPath = requestPath + "&" + admin.SinceIDKey + "=" + sinceID + requestPath = requestPath + "&" + apiutil.SinceIDKey + "=" + sinceID } if minID != "" { - requestPath = requestPath + "&" + admin.MinIDKey + "=" + minID + requestPath = requestPath + "&" + apiutil.MinIDKey + "=" + minID } baseURI := config.GetProtocol() + "://" + config.GetHost() requestURI := baseURI + "/api/" + requestPath @@ -766,7 +767,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() { } ]`, string(b)) - suite.Equal(`; rel="next", ; rel="prev"`, link) + suite.Equal(`; rel="next", ; rel="prev"`, link) } func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() { @@ -1028,8 +1029,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetZeroLimit() { suite.NoError(err) suite.Len(reports, 2) - // Limit in Link header should be set to 100 - suite.Equal(`; rel="next", ; rel="prev"`, link) + // Limit in Link header should be set to default (20) + suite.Equal(`; rel="next", ; rel="prev"`, link) } func (suite *ReportsGetTestSuite) TestReportsGetHighLimit() { diff --git a/internal/api/client/admin/ruledelete.go b/internal/api/client/admin/ruledelete.go index ead219e342..7e8fc0037b 100644 --- a/internal/api/client/admin/ruledelete.go +++ b/internal/api/client/admin/ruledelete.go @@ -18,7 +18,6 @@ package admin import ( - "errors" "fmt" "net/http" @@ -95,10 +94,9 @@ func (m *Module) RuleDELETEHandler(c *gin.Context) { return } - ruleID := c.Param(IDKey) - if ruleID == "" { - err := errors.New("no rule id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + ruleID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/ruleget.go b/internal/api/client/admin/ruleget.go index 8281092fb4..28c0599f50 100644 --- a/internal/api/client/admin/ruleget.go +++ b/internal/api/client/admin/ruleget.go @@ -18,7 +18,6 @@ package admin import ( - "errors" "fmt" "net/http" @@ -85,10 +84,9 @@ func (m *Module) RuleGETHandler(c *gin.Context) { return } - ruleID := c.Param(IDKey) - if ruleID == "" { - err := errors.New("no rule id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + ruleID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/ruleupdate.go b/internal/api/client/admin/ruleupdate.go index bf838f7aec..d58c30d94d 100644 --- a/internal/api/client/admin/ruleupdate.go +++ b/internal/api/client/admin/ruleupdate.go @@ -18,7 +18,6 @@ package admin import ( - "errors" "fmt" "net/http" @@ -87,10 +86,9 @@ func (m *Module) RulePATCHHandler(c *gin.Context) { return } - ruleID := c.Param(IDKey) - if ruleID == "" { - err := errors.New("no rule id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + ruleID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/reports/reportget.go b/internal/api/client/reports/reportget.go index 4a9b066644..c9ca0054f3 100644 --- a/internal/api/client/reports/reportget.go +++ b/internal/api/client/reports/reportget.go @@ -18,7 +18,6 @@ package reports import ( - "errors" "net/http" "github.com/gin-gonic/gin" @@ -77,10 +76,9 @@ func (m *Module) ReportGETHandler(c *gin.Context) { return } - targetReportID := c.Param(IDKey) - if targetReportID == "" { - err := errors.New("no report id specified") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + targetReportID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/reports/reportget_test.go b/internal/api/client/reports/reportget_test.go index 5b6c406f70..4f01101b22 100644 --- a/internal/api/client/reports/reportget_test.go +++ b/internal/api/client/reports/reportget_test.go @@ -145,7 +145,7 @@ func (suite *ReportGetTestSuite) TestGetReport2() { } func (suite *ReportGetTestSuite) TestGetReport3() { - report, err := suite.getReport(http.StatusBadRequest, `{"error":"Bad Request: no report id specified"}`, "") + report, err := suite.getReport(http.StatusBadRequest, `{"error":"Bad Request: required key id was not set or had empty value"}`, "") suite.NoError(err) suite.Nil(report) } diff --git a/internal/api/client/reports/reports.go b/internal/api/client/reports/reports.go index e881474fb4..b10697c1fd 100644 --- a/internal/api/client/reports/reports.go +++ b/internal/api/client/reports/reports.go @@ -21,19 +21,13 @@ import ( "net/http" "github.com/gin-gonic/gin" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/processing" ) const ( - BasePath = "/v1/reports" - IDKey = "id" - ResolvedKey = "resolved" - TargetAccountIDKey = "target_account_id" - MaxIDKey = "max_id" - SinceIDKey = "since_id" - MinIDKey = "min_id" - LimitKey = "limit" - BasePathWithID = BasePath + "/:" + IDKey + BasePath = "/v1/reports" + BasePathWithID = BasePath + "/:" + apiutil.IDKey ) type Module struct { diff --git a/internal/api/client/reports/reportsget.go b/internal/api/client/reports/reportsget.go index 5f194a5898..4c3d4e33ac 100644 --- a/internal/api/client/reports/reportsget.go +++ b/internal/api/client/reports/reportsget.go @@ -18,14 +18,13 @@ package reports 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" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) // ReportsGETHandler swagger:operation GET /api/v1/reports reports @@ -67,7 +66,7 @@ import ( // name: max_id // type: string // description: >- -// Return only reports *OLDER* than the given max ID. +// Return only reports *OLDER* than the given max ID (for paging downwards). // The report with the specified ID will not be included in the response. // in: query // - @@ -76,24 +75,21 @@ import ( // 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. +// Return only reports immediately *NEWER* than the given min ID (for paging upwards). // 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. +// description: Number of reports to return. // default: 20 +// minimum: 1 +// maximum: 100 // in: query // // security: @@ -134,36 +130,29 @@ func (m *Module) ReportsGETHandler(c *gin.Context) { 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.InstanceGetV1) - return - } - resolved = &i + resolved, errWithCode := apiutil.ParseResolved(c.Query(apiutil.ResolvedKey), nil) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return } - 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.InstanceGetV1) - return - } - - // normalize - if i <= 0 { - i = 1 - } else if i >= 100 { - i = 100 - } - limit = i + page, errWithCode := paging.ParseIDPage(c, + 1, // min limit + 100, // max limit + 20, // default limit + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return } - resp, errWithCode := m.processor.Report().GetMultiple(c.Request.Context(), authed.Account, resolved, c.Query(TargetAccountIDKey), c.Query(MaxIDKey), c.Query(SinceIDKey), c.Query(MinIDKey), limit) + resp, errWithCode := m.processor.Report().GetMultiple( + c.Request.Context(), + authed.Account, + resolved, + c.Query(apiutil.TargetAccountIDKey), + page, + ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/reports/reportsget_test.go b/internal/api/client/reports/reportsget_test.go index c63d6c8940..2413292a03 100644 --- a/internal/api/client/reports/reportsget_test.go +++ b/internal/api/client/reports/reportsget_test.go @@ -29,6 +29,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/reports" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -61,21 +62,21 @@ func (suite *ReportsGetTestSuite) getReports( ctx.Set(oauth.SessionAuthorizedUser, user) // create the request URI - requestPath := reports.BasePath + "?" + reports.LimitKey + "=" + strconv.Itoa(limit) + requestPath := reports.BasePath + "?" + apiutil.LimitKey + "=" + strconv.Itoa(limit) if resolved != nil { - requestPath = requestPath + "&" + reports.ResolvedKey + "=" + strconv.FormatBool(*resolved) + requestPath = requestPath + "&" + apiutil.ResolvedKey + "=" + strconv.FormatBool(*resolved) } if targetAccountID != "" { - requestPath = requestPath + "&" + reports.TargetAccountIDKey + "=" + targetAccountID + requestPath = requestPath + "&" + apiutil.TargetAccountIDKey + "=" + targetAccountID } if maxID != "" { - requestPath = requestPath + "&" + reports.MaxIDKey + "=" + maxID + requestPath = requestPath + "&" + apiutil.MaxIDKey + "=" + maxID } if sinceID != "" { - requestPath = requestPath + "&" + reports.SinceIDKey + "=" + sinceID + requestPath = requestPath + "&" + apiutil.SinceIDKey + "=" + sinceID } if minID != "" { - requestPath = requestPath + "&" + reports.MinIDKey + "=" + minID + requestPath = requestPath + "&" + apiutil.MinIDKey + "=" + minID } baseURI := config.GetProtocol() + "://" + config.GetHost() requestURI := baseURI + "/api/" + requestPath diff --git a/internal/api/client/search/searchget.go b/internal/api/client/search/searchget.go index d7ab813880..0f9595efc6 100644 --- a/internal/api/client/search/searchget.go +++ b/internal/api/client/search/searchget.go @@ -247,7 +247,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) { Resolve: resolve, Following: following, ExcludeUnreviewed: excludeUnreviewed, - AccountID: c.Query(apiutil.SearchAccountIDKey), + AccountID: c.Query(apiutil.AccountIDKey), APIv1: apiVersion == apiutil.APIv1, } diff --git a/internal/api/client/search/searchget_test.go b/internal/api/client/search/searchget_test.go index a0d0cad0ed..27e5f782d5 100644 --- a/internal/api/client/search/searchget_test.go +++ b/internal/api/client/search/searchget_test.go @@ -105,7 +105,7 @@ func (suite *SearchGetTestSuite) getSearch( } if fromAccountID != nil { - queryParts = append(queryParts, apiutil.SearchAccountIDKey+"="+url.QueryEscape(*fromAccountID)) + queryParts = append(queryParts, apiutil.AccountIDKey+"="+url.QueryEscape(*fromAccountID)) } requestURL.RawQuery = strings.Join(queryParts, "&") diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go index 5210735a14..90cc30e6fb 100644 --- a/internal/api/util/parsequery.go +++ b/internal/api/util/parsequery.go @@ -34,13 +34,16 @@ const ( /* Common keys */ - IDKey = "id" - LimitKey = "limit" - LocalKey = "local" - MaxIDKey = "max_id" - SinceIDKey = "since_id" - MinIDKey = "min_id" - UsernameKey = "username" + IDKey = "id" + LimitKey = "limit" + LocalKey = "local" + MaxIDKey = "max_id" + SinceIDKey = "since_id" + MinIDKey = "min_id" + UsernameKey = "username" + AccountIDKey = "account_id" + TargetAccountIDKey = "target_account_id" + ResolvedKey = "resolved" /* AP endpoint keys */ @@ -55,7 +58,6 @@ const ( SearchQueryKey = "q" SearchResolveKey = "resolve" SearchTypeKey = "type" - SearchAccountIDKey = "account_id" /* Tag keys */ @@ -132,6 +134,10 @@ func ParseLocal(value string, defaultValue bool) (bool, gtserror.WithCode) { return parseBool(value, defaultValue, LocalKey) } +func ParseResolved(value string, defaultValue *bool) (*bool, gtserror.WithCode) { + return parseBoolPtr(value, defaultValue, ResolvedKey) +} + func ParseSearchExcludeUnreviewed(value string, defaultValue bool) (bool, gtserror.WithCode) { return parseBool(value, defaultValue, SearchExcludeUnreviewedKey) } @@ -289,6 +295,19 @@ func parseBool(value string, defaultValue bool, key string) (bool, gtserror.With return i, nil } +func parseBoolPtr(value string, defaultValue *bool, key string) (*bool, gtserror.WithCode) { + if value == "" { + return defaultValue, nil + } + + i, err := strconv.ParseBool(value) + if err != nil { + return defaultValue, parseError(key, value, defaultValue, err) + } + + return &i, nil +} + func parseInt(value string, defaultValue int, max int, min int, key string) (int, gtserror.WithCode) { if value == "" { return defaultValue, nil diff --git a/internal/db/bundb/report.go b/internal/db/bundb/report.go index 486bf09f00..db36ae192f 100644 --- a/internal/db/bundb/report.go +++ b/internal/db/bundb/report.go @@ -20,6 +20,7 @@ package bundb import ( "context" "errors" + "slices" "time" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -27,6 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/uptrace/bun" ) @@ -51,12 +53,22 @@ func (r *reportDB) GetReportByID(ctx context.Context, id string) (*gtsmodel.Repo ) } -func (r *reportDB) GetReports(ctx context.Context, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) ([]*gtsmodel.Report, error) { - reportIDs := []string{} +func (r *reportDB) GetReports(ctx context.Context, resolved *bool, accountID string, targetAccountID string, page *paging.Page) ([]*gtsmodel.Report, error) { + var ( + // Get paging params. + minID = page.GetMin() + maxID = page.GetMax() + limit = page.GetLimit() + order = page.GetOrder() + + // Make educated guess for slice size + reportIDs = make([]string, 0, limit) + ) q := r.db. NewSelect(). TableExpr("? AS ?", bun.Ident("reports"), bun.Ident("report")). + // Select only IDs from table. Column("report.id"). Order("report.id DESC") @@ -77,22 +89,32 @@ func (r *reportDB) GetReports(ctx context.Context, resolved *bool, accountID str q = q.Where("? = ?", bun.Ident("report.target_account_id"), targetAccountID) } + // Return only reports with id + // lower than provided maxID. if maxID != "" { q = q.Where("? < ?", bun.Ident("report.id"), maxID) } - if sinceID != "" { - q = q.Where("? > ?", bun.Ident("report.id"), minID) - } - + // Return only reports with id + // greater than provided minID. if minID != "" { - q = q.Where("? > ?", bun.Ident("report.id"), minID) + q = q.Where("? > ?", bun.Ident("report.id"), maxID) } - if limit != 0 { + if limit > 0 { + // Limit amount of + // reports returned. q = q.Limit(limit) } + if order == paging.OrderAscending { + // Page up. + q = q.OrderExpr("? ASC", bun.Ident("report.id")) + } else { + // Page down. + q = q.OrderExpr("? DESC", bun.Ident("report.id")) + } + if err := q.Scan(ctx, &reportIDs); err != nil { return nil, err } @@ -102,6 +124,12 @@ func (r *reportDB) GetReports(ctx context.Context, resolved *bool, accountID str return nil, db.ErrNoEntries } + // If we're paging up, we still want reports + // to be sorted by ID desc, so reverse ids slice. + if order == paging.OrderAscending { + slices.Reverse(reportIDs) + } + // Allocate return slice (will be at most len reportIDs) reports := make([]*gtsmodel.Report, 0, len(reportIDs)) for _, id := range reportIDs { diff --git a/internal/db/bundb/report_test.go b/internal/db/bundb/report_test.go index 594b0b7aaf..eec51d271d 100644 --- a/internal/db/bundb/report_test.go +++ b/internal/db/bundb/report_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -61,14 +62,26 @@ func (suite *ReportTestSuite) TestGetReportByURI() { } func (suite *ReportTestSuite) TestGetAllReports() { - reports, err := suite.db.GetReports(context.Background(), nil, "", "", "", "", "", 0) + reports, err := suite.db.GetReports( + context.Background(), + nil, + "", + "", + &paging.Page{}, + ) suite.NoError(err) suite.NotEmpty(reports) } func (suite *ReportTestSuite) TestGetAllReportsByAccountID() { accountID := suite.testAccounts["local_account_2"].ID - reports, err := suite.db.GetReports(context.Background(), nil, accountID, "", "", "", "", 0) + reports, err := suite.db.GetReports( + context.Background(), + nil, + accountID, + "", + &paging.Page{}, + ) suite.NoError(err) suite.NotEmpty(reports) for _, r := range reports { diff --git a/internal/db/report.go b/internal/db/report.go index a04b4d3fa4..91b3681065 100644 --- a/internal/db/report.go +++ b/internal/db/report.go @@ -21,6 +21,7 @@ import ( "context" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) // Report handles getting/creation/deletion/updating of user reports/flags. @@ -30,7 +31,7 @@ type Report interface { // GetReports gets limit n reports using the given parameters. // Parameters that are empty / zero are ignored. - GetReports(ctx context.Context, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) ([]*gtsmodel.Report, error) + GetReports(ctx context.Context, resolved *bool, accountID string, targetAccountID string, page *paging.Page) ([]*gtsmodel.Report, error) // PopulateReport populates the struct pointers on the given report. PopulateReport(ctx context.Context, report *gtsmodel.Report) error diff --git a/internal/processing/admin/report.go b/internal/processing/admin/report.go index d2186cfa20..13b5a9d86a 100644 --- a/internal/processing/admin/report.go +++ b/internal/processing/admin/report.go @@ -21,73 +21,81 @@ import ( "context" "errors" "fmt" + "net/url" "strconv" "time" "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) -// ReportsGet returns all reports stored on this instance, with the given parameters. +// ReportsGet returns reports stored on this +// instance, with the given parameters. func (p *Processor) ReportsGet( ctx context.Context, account *gtsmodel.Account, resolved *bool, accountID string, targetAccountID string, - maxID string, - sinceID string, - minID string, - limit int, + page *paging.Page, ) (*apimodel.PageableResponse, gtserror.WithCode) { - reports, err := p.state.DB.GetReports(ctx, resolved, accountID, targetAccountID, maxID, sinceID, minID, limit) + reports, err := p.state.DB.GetReports( + ctx, + resolved, + accountID, + targetAccountID, + page, + ) if err != nil && !errors.Is(err, db.ErrNoEntries) { return nil, gtserror.NewErrorInternalError(err) } count := len(reports) if count == 0 { - return util.EmptyPageableResponse(), nil + return paging.EmptyResponse(), nil } - var ( - items = make([]interface{}, 0, count) - nextMaxIDValue = reports[count-1].ID - prevMinIDValue = reports[0].ID - ) + // Get the lowest and highest + // ID values, used for paging. + lo := reports[count-1].ID + hi := reports[0].ID + // Convert each report to API model. + items := make([]interface{}, 0, count) for _, r := range reports { item, err := p.converter.ReportToAdminAPIReport(ctx, r, account) if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) + err := fmt.Errorf("error converting report to api: %s", err) + return nil, gtserror.NewErrorInternalError(err) } items = append(items, item) } - extraQueryParams := make([]string, 0, 3) + // Assemble next/prev page queries. + query := make(url.Values, 3) if resolved != nil { - extraQueryParams = append(extraQueryParams, "resolved="+strconv.FormatBool(*resolved)) + query.Set(apiutil.ResolvedKey, strconv.FormatBool(*resolved)) } if accountID != "" { - extraQueryParams = append(extraQueryParams, "account_id="+accountID) + query.Set(apiutil.AccountIDKey, accountID) } if targetAccountID != "" { - extraQueryParams = append(extraQueryParams, "target_account_id="+targetAccountID) + query.Set(apiutil.TargetAccountIDKey, targetAccountID) } - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "/api/v1/admin/reports", - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - Limit: limit, - ExtraQueryParams: extraQueryParams, - }) + return paging.PackageResponse(paging.ResponseParams{ + Items: items, + Path: "/api/v1/admin/reports", + Next: page.Next(lo, hi), + Prev: page.Prev(lo, hi), + Query: query, + }), nil } // ReportGet returns one report, with the given ID. diff --git a/internal/processing/report/get.go b/internal/processing/report/get.go index c5c4fc223c..2e3c1b2dc1 100644 --- a/internal/processing/report/get.go +++ b/internal/processing/report/get.go @@ -21,13 +21,15 @@ import ( "context" "errors" "fmt" + "net/url" "strconv" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) // Get returns the user view of a moderation report, with the given id. @@ -53,53 +55,61 @@ func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, id strin return apiReport, nil } -// GetMultiple returns multiple reports created by the given account, filtered according to the provided parameters. +// GetMultiple returns reports created by the given account, +// filtered according to the provided parameters. func (p *Processor) GetMultiple( ctx context.Context, account *gtsmodel.Account, resolved *bool, targetAccountID string, - maxID string, - sinceID string, - minID string, - limit int, + page *paging.Page, ) (*apimodel.PageableResponse, gtserror.WithCode) { - reports, err := p.state.DB.GetReports(ctx, resolved, account.ID, targetAccountID, maxID, sinceID, minID, limit) + reports, err := p.state.DB.GetReports( + ctx, + resolved, + account.ID, + targetAccountID, + page, + ) if err != nil && !errors.Is(err, db.ErrNoEntries) { return nil, gtserror.NewErrorInternalError(err) } count := len(reports) if count == 0 { - return util.EmptyPageableResponse(), nil + return paging.EmptyResponse(), nil } - items := make([]interface{}, 0, count) - nextMaxIDValue := reports[count-1].ID - prevMinIDValue := reports[0].ID + // Get the lowest and highest + // ID values, used for paging. + lo := reports[count-1].ID + hi := reports[0].ID + // Convert each report to API model. + items := make([]interface{}, 0, count) for _, r := range reports { item, err := p.converter.ReportToAPIReport(ctx, r) if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) + err := fmt.Errorf("error converting report to api: %s", err) + return nil, gtserror.NewErrorInternalError(err) } items = append(items, item) } - extraQueryParams := []string{} + // Assemble next/prev page queries. + query := make(url.Values, 3) if resolved != nil { - extraQueryParams = append(extraQueryParams, "resolved="+strconv.FormatBool(*resolved)) + query.Set(apiutil.ResolvedKey, strconv.FormatBool(*resolved)) } if targetAccountID != "" { - extraQueryParams = append(extraQueryParams, "target_account_id="+targetAccountID) + query.Set(apiutil.TargetAccountIDKey, targetAccountID) } - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "/api/v1/reports", - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - Limit: limit, - ExtraQueryParams: extraQueryParams, - }) + return paging.PackageResponse(paging.ResponseParams{ + Items: items, + Path: "/api/v1/reports", + Next: page.Next(lo, hi), + Prev: page.Prev(lo, hi), + Query: query, + }), nil } From 6abf462f0dc39f6d278026e366852d6022bbdead Mon Sep 17 00:00:00 2001 From: tobi Date: Sun, 16 Jun 2024 18:54:47 +0200 Subject: [PATCH 2/8] we're making it happen --- .../settings/lib/query/admin/reports/index.ts | 52 +++-- web/source/settings/lib/query/gts-api.ts | 2 +- web/source/settings/lib/types/report.ts | 18 +- .../views/moderation/reports/overview.tsx | 97 -------- .../views/moderation/reports/search.tsx | 215 ++++++++++++++++++ .../settings/views/moderation/router.tsx | 5 +- 6 files changed, 269 insertions(+), 120 deletions(-) delete mode 100644 web/source/settings/views/moderation/reports/overview.tsx create mode 100644 web/source/settings/views/moderation/reports/search.tsx diff --git a/web/source/settings/lib/query/admin/reports/index.ts b/web/source/settings/lib/query/admin/reports/index.ts index 600e78ac3b..8937d53589 100644 --- a/web/source/settings/lib/query/admin/reports/index.ts +++ b/web/source/settings/lib/query/admin/reports/index.ts @@ -21,29 +21,51 @@ import { gtsApi } from "../../gts-api"; import type { AdminReport, - AdminReportListParams, + AdminSearchReportParams, AdminReportResolveParams, + AdminSearchReportResp, } from "../../../types/report"; +import parse from "parse-link-header"; const extended = gtsApi.injectEndpoints({ endpoints: (build) => ({ - listReports: build.query({ - query: (params) => ({ - url: "/api/v1/admin/reports", - params: { - // Override provided limit. - limit: 100, - ...params + searchReports: build.query({ + query: (form) => { + const params = new(URLSearchParams); + Object.entries(form).forEach(([k, v]) => { + if (v !== undefined) { + params.append(k, v); + } + }); + + let query = ""; + if (params.size !== 0) { + query = `?${params.toString()}`; } - }), - providesTags: [{ type: "Reports", id: "LIST" }] + + return { + url: `/api/v1/admin/reports${query}` + }; + }, + // Headers required for paging. + transformResponse: (apiResp: AdminReport[], meta) => { + const accounts = apiResp; + const linksStr = meta?.response?.headers.get("Link"); + const links = parse(linksStr); + return { accounts, links }; + }, + // Only provide LIST tag id since this model is not the + // same as getReport model (due to transformResponse). + providesTags: [{ type: "Report", id: "TRANSFORMED" }] }), getReport: build.query({ query: (id) => ({ url: `/api/v1/admin/reports/${id}` }), - providesTags: (_res, _error, id) => [{ type: "Reports", id }] + providesTags: (_result, _error, id) => [ + { type: 'Report', id } + ], }), resolveReport: build.mutation({ @@ -55,8 +77,8 @@ const extended = gtsApi.injectEndpoints({ }), invalidatesTags: (res) => res - ? [{ type: "Reports", id: "LIST" }, { type: "Reports", id: res.id }] - : [{ type: "Reports", id: "LIST" }] + ? [{ type: "Report", id: "LIST" }, { type: "Report", id: res.id }] + : [{ type: "Report", id: "LIST" }] }) }) }); @@ -64,7 +86,7 @@ const extended = gtsApi.injectEndpoints({ /** * List reports received on this instance, filtered using given parameters. */ -const useListReportsQuery = extended.useListReportsQuery; +const useLazySearchReportsQuery = extended.useLazySearchReportsQuery; /** * Get a single report by its ID. @@ -77,7 +99,7 @@ const useGetReportQuery = extended.useGetReportQuery; const useResolveReportMutation = extended.useResolveReportMutation; export { - useListReportsQuery, + useLazySearchReportsQuery, useGetReportQuery, useResolveReportMutation, }; diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts index ef994e655d..f96a55fda9 100644 --- a/web/source/settings/lib/query/gts-api.ts +++ b/web/source/settings/lib/query/gts-api.ts @@ -136,7 +136,7 @@ export const gtsApi = createApi({ tagTypes: [ "Auth", "Emoji", - "Reports", + "Report", "Account", "InstanceRules", "HTTPHeaderAllows", diff --git a/web/source/settings/lib/types/report.ts b/web/source/settings/lib/types/report.ts index bb3d53c27d..00b08398f0 100644 --- a/web/source/settings/lib/types/report.ts +++ b/web/source/settings/lib/types/report.ts @@ -17,6 +17,9 @@ along with this program. If not, see . */ +import { Links } from "parse-link-header"; +import { AdminAccount } from "./account"; + /** * Admin model of a report. Differs from the client * model, which contains less detailed information. @@ -58,22 +61,22 @@ export interface AdminReport { * Account that created the report. * TODO: model this properly. */ - account: Object; + account: AdminAccount; /** * Reported account. * TODO: model this properly. */ - target_account: Object; + target_account: AdminAccount; /** * Admin account assigned to handle this report, if any. * TODO: model this properly. */ - assigned_account?: Object; + assigned_account?: AdminAccount; /** * Admin account that has taken action on this report, if any. * TODO: model this properly. */ - action_taken_by_account?: Object; + action_taken_by_account?: AdminAccount; /** * Statuses cited by this report, if any. * TODO: model this properly. @@ -108,7 +111,7 @@ export interface AdminReportResolveParams { /** * Parameters for GET to /api/v1/admin/reports. */ -export interface AdminReportListParams { +export interface AdminSearchReportParams { /** * If set, show only resolved (true) or only unresolved (false) reports. */ @@ -142,3 +145,8 @@ export interface AdminReportListParams { */ limit?: number; } + +export interface AdminSearchReportResp { + accounts: AdminReport[]; + links: Links | null; +} diff --git a/web/source/settings/views/moderation/reports/overview.tsx b/web/source/settings/views/moderation/reports/overview.tsx deleted file mode 100644 index 18eb5492ac..0000000000 --- a/web/source/settings/views/moderation/reports/overview.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - GoToSocial - Copyright (C) GoToSocial Authors admin@gotosocial.org - SPDX-License-Identifier: AGPL-3.0-or-later - - 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 . -*/ - -import React from "react"; -import { Link } from "wouter"; -import FormWithData from "../../../lib/form/form-with-data"; -import Username from "../../../components/username"; -import { useListReportsQuery } from "../../../lib/query/admin/reports"; - -export function ReportOverview({ }) { - return ( - - ); -} - -function ReportsList({ data: reports }) { - return ( -
-
-

Reports

-

- Here you can view and resolve reports made to your - instance, originating from local and remote users. -

- - Learn more about this (opens in a new tab) - -
-
- {reports.map((report) => ( - - ))} -
-
- ); -} - -function ReportEntry({ report }) { - const from = report.account; - const target = report.target_account; - - let comment = report.comment.length > 200 - ? report.comment.slice(0, 200) + "..." - : report.comment; - - return ( - -
-
-
- reported -
-

- {report.action_taken ? "Resolved" : "Open"} -

-
-
- Created: - {new Date(report.created_at).toLocaleString()} - - Reason: - {comment.length > 0 - ?

{comment}

- : none provided - } -
-
- - ); -} diff --git a/web/source/settings/views/moderation/reports/search.tsx b/web/source/settings/views/moderation/reports/search.tsx new file mode 100644 index 0000000000..ec0f3bf35a --- /dev/null +++ b/web/source/settings/views/moderation/reports/search.tsx @@ -0,0 +1,215 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + 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 . +*/ + +import React, { ReactNode, useEffect, useMemo } from "react"; + +import { useLazySearchReportsQuery } from "../../../lib/query/admin/reports"; +import { useTextInput } from "../../../lib/form"; +import { PageableList } from "../../../components/pageable-list"; +import { Select } from "../../../components/form/inputs"; +import MutationButton from "../../../components/form/mutation-button"; +import { Link, useLocation, useSearch } from "wouter"; +import Username from "../../../components/username"; +import { AdminReport } from "../../../lib/types/report"; + +export default function ReportsSearch() { + return ( +
+

Reports Search

+ + You can use the form below to search through reports + created by, or directed towards, accounts on this instance. + + +
+ ); +} + +function ReportSearchForm() { + const [ location, setLocation ] = useLocation(); + const search = useSearch(); + const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]); + const [ searchReports, searchRes ] = useLazySearchReportsQuery(); + + // Populate search form using values from + // urlQueryParams, to allow paging. + const form = { + resolved: useTextInput("resolved", { defaultValue: urlQueryParams.get("resolved") ?? "" }), + account_id: useTextInput("account_id", { defaultValue: urlQueryParams.get("account_id") ?? "" }), + target_account_id: useTextInput("target_account_id", { defaultValue: urlQueryParams.get("target_account_id") ?? "" }), + limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "20" }) + }; + + // On mount, if urlQueryParams were provided, + // trigger the search. For example, if page + // was accessed at /search?origin=local&limit=20, + // then run a search with origin=local and + // limit=20 and immediately render the results. + useEffect(() => { + if (urlQueryParams.size > 0) { + searchReports(Object.fromEntries(urlQueryParams), true); + } + }, [urlQueryParams, searchReports]); + + // Rather than triggering the search directly, + // the "submit" button changes the location + // based on form field params, and lets the + // useEffect hook above actually do the search. + function submitQuery(e) { + e.preventDefault(); + + // Parse query parameters. + const entries = Object.entries(form).map(([k, v]) => { + // Take only defined form fields. + if (v.value === undefined || v.value.length === 0) { + return null; + } + return [[k, v.value]]; + }).flatMap(kv => { + // Remove any nulls. + return kv || []; + }); + + const searchParams = new URLSearchParams(entries); + setLocation(location + "?" + searchParams.toString()); + } + + // Location to return to when user clicks "back" on the detail view. + const backLocation = location + (urlQueryParams ? `?${urlQueryParams}` : ""); + + // Function to map an item to a list entry. + function itemToEntry(report: AdminReport): ReactNode { + return ( + + ); + } + + return ( + <> +
+ + + + No reports found that match your query.} + prevNextLinks={searchRes.data?.links} + /> + + ); +} + + +// function ReportsList({ data: reports }) { +// return ( +//
+//
+//

Reports

+//

+// Here you can view and resolve reports made to your +// instance, originating from local and remote users. +//

+// +// Learn more about this (opens in a new tab) +// +//
+//
+// {reports.map((report) => ( +// +// ))} +//
+//
+// ); +// } + +interface ReportEntryProps { + report: AdminReport; + linkTo?: string; + backLocation?: string; +} + +function ReportEntry({ report }: ReportEntryProps) { + const from = report.account; + const target = report.target_account; + + let comment = report.comment.length > 200 + ? report.comment.slice(0, 200) + "..." + : report.comment; + + return ( + +
+
+
+ reported +
+

+ {report.action_taken ? "Resolved" : "Open"} +

+
+
+ Created: + {new Date(report.created_at).toLocaleString()} + + Reason: + {comment.length > 0 + ?

{comment}

+ : none provided + } +
+
+ + ); +} diff --git a/web/source/settings/views/moderation/router.tsx b/web/source/settings/views/moderation/router.tsx index d23ab336a7..93f7e481ac 100644 --- a/web/source/settings/views/moderation/router.tsx +++ b/web/source/settings/views/moderation/router.tsx @@ -20,7 +20,7 @@ import React from "react"; import { BaseUrlContext, useBaseUrl, useHasPermission } from "../../lib/navigation/util"; import { Redirect, Route, Router, Switch } from "wouter"; -import { ReportOverview } from "./reports/overview"; +import ReportsSearch from "./reports/search"; import ReportDetail from "./reports/detail"; import { ErrorBoundary } from "../../lib/navigation/error"; import ImportExport from "./domain-permissions/import-export"; @@ -85,8 +85,9 @@ function ModerationReportsRouter() { + - + From f6e32ecee0f8e48d9cdd113346787d447a8bbe43 Mon Sep 17 00:00:00 2001 From: tobi Date: Mon, 17 Jun 2024 17:48:58 +0200 Subject: [PATCH 3/8] fix little whoopsie --- internal/db/bundb/report.go | 5 +- internal/db/bundb/report_test.go | 84 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/internal/db/bundb/report.go b/internal/db/bundb/report.go index db36ae192f..f99f0b5cc5 100644 --- a/internal/db/bundb/report.go +++ b/internal/db/bundb/report.go @@ -69,8 +69,7 @@ func (r *reportDB) GetReports(ctx context.Context, resolved *bool, accountID str NewSelect(). TableExpr("? AS ?", bun.Ident("reports"), bun.Ident("report")). // Select only IDs from table. - Column("report.id"). - Order("report.id DESC") + Column("report.id") if resolved != nil { i := bun.Ident("report.action_taken_by_account_id") @@ -98,7 +97,7 @@ func (r *reportDB) GetReports(ctx context.Context, resolved *bool, accountID str // Return only reports with id // greater than provided minID. if minID != "" { - q = q.Where("? > ?", bun.Ident("report.id"), maxID) + q = q.Where("? > ?", bun.Ident("report.id"), minID) } if limit > 0 { diff --git a/internal/db/bundb/report_test.go b/internal/db/bundb/report_test.go index eec51d271d..1a488c7296 100644 --- a/internal/db/bundb/report_test.go +++ b/internal/db/bundb/report_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" @@ -73,6 +74,89 @@ func (suite *ReportTestSuite) TestGetAllReports() { suite.NotEmpty(reports) } +func (suite *ReportTestSuite) TestReportPagingDown() { + // Get one from the top. + reports1, err := suite.db.GetReports( + context.Background(), + nil, + "", + "", + &paging.Page{ + Limit: 1, + }, + ) + if err != nil { + suite.FailNow(err.Error()) + } + if l := len(reports1); l != 1 { + suite.FailNowf("", "expected reports len 1, got %d", l) + } + id1 := reports1[0].ID + + // Use this one to page down. + reports2, err := suite.db.GetReports( + context.Background(), + nil, + "", + "", + &paging.Page{ + Limit: 1, + Max: paging.MaxID(id1), + }, + ) + if err != nil { + suite.FailNow(err.Error()) + } + if l := len(reports2); l != 1 { + suite.FailNowf("", "expected reports len 1, got %d", l) + } + id2 := reports2[0].ID + + suite.Greater(id1, id2) +} + +func (suite *ReportTestSuite) TestReportPagingUp() { + // Get one from the bottom. + reports1, err := suite.db.GetReports( + context.Background(), + nil, + "", + "", + &paging.Page{ + Limit: 1, + Min: paging.MinID(id.Lowest), + }, + ) + if err != nil { + suite.FailNow(err.Error()) + } + if l := len(reports1); l != 1 { + suite.FailNowf("", "expected reports len 1, got %d", l) + } + id1 := reports1[0].ID + + // Use this one to page up. + reports2, err := suite.db.GetReports( + context.Background(), + nil, + "", + "", + &paging.Page{ + Limit: 1, + Min: paging.MinID(id1), + }, + ) + if err != nil { + suite.FailNow(err.Error()) + } + if l := len(reports2); l != 1 { + suite.FailNowf("", "expected reports len 1, got %d", l) + } + id2 := reports2[0].ID + + suite.Less(id1, id2) +} + func (suite *ReportTestSuite) TestGetAllReportsByAccountID() { accountID := suite.testAccounts["local_account_2"].ID reports, err := suite.db.GetReports( From c9093bf9a056a7bd937777563f85959df7a4f7d1 Mon Sep 17 00:00:00 2001 From: tobi Date: Mon, 17 Jun 2024 17:49:20 +0200 Subject: [PATCH 4/8] styling for report list --- web/source/settings/style.css | 134 +++++++++--------- .../views/moderation/reports/search.tsx | 110 +++++++------- 2 files changed, 124 insertions(+), 120 deletions(-) diff --git a/web/source/settings/style.css b/web/source/settings/style.css index d2420bdfc1..0a0b1cb914 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -1045,62 +1045,62 @@ button.with-padding { } } -.reports { - p { - margin: 0; - } - +.reports-view { .report { display: flex; flex-direction: column; + flex-wrap: nowrap; gap: 0.5rem; - margin: 0.5rem 0; - - text-decoration: none; color: $fg; - - padding: 1rem; - - border: none; border-left: 0.3rem solid $border-accent; - .usernames { - line-height: 2rem; - } - - .byline { - display: grid; - grid-template-columns: 1fr auto; - gap: 0.5rem; + .username-lozenge { + display: flex; + flex-wrap: nowrap; + height: 100%; + align-items: center; + padding-top: 0; + padding-bottom: 0; - .report-status { - color: $border-accent; + .fa { + flex-shrink: 0; } } - .details { - display: grid; - grid-template-columns: auto 1fr; - gap: 0.2rem 0.5rem; - padding: 0.5rem; - - justify-items: start; + .report-byline { + max-width: fit-content; } - h3 { - margin: 0; + .info-list { + border: none; + + .info-list-entry { + background: none; + padding: 0; + + .report-target .username-lozenge { + color: $bg; + } + + .reported-by .username-lozenge { + color: $fg; + font-weight: initial; + border-radius: 0; + background: none; + } + } } &.resolved { - color: $fg-reduced; - border-left: 0.4rem solid $bg; + border-left: 0.3rem solid $list-entry-bg; - .byline .report-status { + .info-list, + .info-list .info-list-entry .reported-by .username-lozenge { color: $fg-reduced; } - - .user { - opacity: 0.8; + + &:hover { + border-color: $fg-accent; } } @@ -1145,36 +1145,6 @@ button.with-padding { } } -.username-lozenge { - line-height: 1.3rem; - display: inline-block; - background: $fg-accent; - color: $bg; - border-radius: $br; - padding: 0.15rem; - font-weight: bold; - text-decoration: none; - - .acct { - word-break: break-all; - } - - &.suspended { - background: $bg-accent; - color: $fg; - text-decoration: line-through; - } - - &.local { - background: $green1; - } -} - -.spanlink { - cursor: pointer; - text-decoration: none; -} - .accounts-view { .pageable-list { .username-lozenge { @@ -1223,6 +1193,36 @@ button.with-padding { } } +.username-lozenge { + line-height: 1.3rem; + display: inline-block; + background: $fg-accent; + color: $bg; + border-radius: $br; + padding: 0.15rem; + font-weight: bold; + text-decoration: none; + + .acct { + word-break: break-all; + } + + &.suspended { + background: $bg-accent; + color: $fg; + text-decoration: line-through; + } + + &.local { + background: $green1; + } +} + +.spanlink { + cursor: pointer; + text-decoration: none; +} + .info-list { border: 0.1rem solid $gray1; display: flex; diff --git a/web/source/settings/views/moderation/reports/search.tsx b/web/source/settings/views/moderation/reports/search.tsx index ec0f3bf35a..a78af847c2 100644 --- a/web/source/settings/views/moderation/reports/search.tsx +++ b/web/source/settings/views/moderation/reports/search.tsx @@ -50,7 +50,7 @@ function ReportSearchForm() { // Populate search form using values from // urlQueryParams, to allow paging. const form = { - resolved: useTextInput("resolved", { defaultValue: urlQueryParams.get("resolved") ?? "" }), + resolved: useTextInput("resolved", { defaultValue: urlQueryParams.get("resolved") ?? "false" }), account_id: useTextInput("account_id", { defaultValue: urlQueryParams.get("account_id") ?? "" }), target_account_id: useTextInput("target_account_id", { defaultValue: urlQueryParams.get("target_account_id") ?? "" }), limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "20" }) @@ -61,9 +61,14 @@ function ReportSearchForm() { // was accessed at /search?origin=local&limit=20, // then run a search with origin=local and // limit=20 and immediately render the results. + // + // If no urlQueryParams set, use the default + // search (just show unresolved reports). useEffect(() => { if (urlQueryParams.size > 0) { searchReports(Object.fromEntries(urlQueryParams), true); + } else { + searchReports({resolved: false}, true); } }, [urlQueryParams, searchReports]); @@ -116,9 +121,9 @@ function ReportSearchForm() { label="Report status" options={ <> - + } > @@ -143,34 +148,6 @@ function ReportSearchForm() { ); } - -// function ReportsList({ data: reports }) { -// return ( -//
-//
-//

Reports

-//

-// Here you can view and resolve reports made to your -// instance, originating from local and remote users. -//

-// -// Learn more about this (opens in a new tab) -// -//
-//
-// {reports.map((report) => ( -// -// ))} -//
-//
-// ); -// } - interface ReportEntryProps { report: AdminReport; linkTo?: string; @@ -180,36 +157,63 @@ interface ReportEntryProps { function ReportEntry({ report }: ReportEntryProps) { const from = report.account; const target = report.target_account; - - let comment = report.comment.length > 200 - ? report.comment.slice(0, 200) + "..." - : report.comment; + const comment = report.comment; + const status = report.action_taken ? "Resolved" : "Unresolved"; + const created = new Date(report.created_at).toLocaleString(); + const title = `${status}. @${from.account.acct} reported @${target.account.acct} on ${created}. Reason: "${comment}"`; return ( -
-
-
- reported -
-

- {report.action_taken ? "Resolved" : "Open"} -

+
+
+
Target
+
+ +
-
- Created: - {new Date(report.created_at).toLocaleString()} - - Reason: - {comment.length > 0 - ?

{comment}

- : none provided - } + +
+
Reported by
+
+ +
+
+ +
+
Status
+
+ { report.action_taken + ? <>{status} + : {status} + } +
+
+ +
+
Reason
+
+ { comment.length > 0 + ? <>{comment} + : none provided + } +
+
+ +
+
Created
+
+ +
-
+
); } From 657ae49cc5e699ed0df3a846ec892455592797ca Mon Sep 17 00:00:00 2001 From: tobi Date: Mon, 17 Jun 2024 21:54:22 +0200 Subject: [PATCH 5/8] don't youuuu forget about meee don't don't don't don't --- .../{fake-profile.tsx => profile.tsx} | 0 .../components/{fake-toot.tsx => status.tsx} | 2 +- web/source/settings/components/username.tsx | 2 +- web/source/settings/lib/types/report.ts | 4 - .../detail/util.tsx => lib/util/index.ts} | 4 +- web/source/settings/style.css | 22 ++- .../views/admin/emoji/local/detail.tsx | 6 +- .../views/admin/emoji/local/new-emoji.tsx | 6 +- .../http-header-permissions/overview.tsx | 2 +- .../moderation/accounts/detail/index.tsx | 4 +- .../moderation/accounts/search/index.tsx | 2 +- .../views/moderation/reports/detail.tsx | 170 ++++++++++++------ .../views/moderation/reports/search.tsx | 65 +++++-- web/source/settings/views/user/profile.tsx | 2 +- 14 files changed, 201 insertions(+), 90 deletions(-) rename web/source/settings/components/{fake-profile.tsx => profile.tsx} (100%) rename web/source/settings/components/{fake-toot.tsx => status.tsx} (97%) rename web/source/settings/{views/moderation/accounts/detail/util.tsx => lib/util/index.ts} (92%) diff --git a/web/source/settings/components/fake-profile.tsx b/web/source/settings/components/profile.tsx similarity index 100% rename from web/source/settings/components/fake-profile.tsx rename to web/source/settings/components/profile.tsx diff --git a/web/source/settings/components/fake-toot.tsx b/web/source/settings/components/status.tsx similarity index 97% rename from web/source/settings/components/fake-toot.tsx rename to web/source/settings/components/status.tsx index ad0c387a43..d2d8106d01 100644 --- a/web/source/settings/components/fake-toot.tsx +++ b/web/source/settings/components/status.tsx @@ -20,7 +20,7 @@ import React from "react"; import { useVerifyCredentialsQuery } from "../lib/query/oauth"; -export default function FakeToot({ children }) { +export default function FakeStatus({ children }) { const { data: account = { avatar: "/assets/default_avatars/GoToSocial_icon1.png", display_name: "", diff --git a/web/source/settings/components/username.tsx b/web/source/settings/components/username.tsx index f7be1cd4a8..56ba67c4f2 100644 --- a/web/source/settings/components/username.tsx +++ b/web/source/settings/components/username.tsx @@ -60,7 +60,7 @@ export default function Username({ account, linkTo, backLocation, classNames }: ); if (linkTo) { - className += " spanlink"; + className += " pseudolink"; return ( - + Look at this new custom emoji {emoji.shortcode} isn't it cool? - + {result.error && } {deleteResult.error && } diff --git a/web/source/settings/views/admin/emoji/local/new-emoji.tsx b/web/source/settings/views/admin/emoji/local/new-emoji.tsx index 20f45f3720..99035e9bf7 100644 --- a/web/source/settings/views/admin/emoji/local/new-emoji.tsx +++ b/web/source/settings/views/admin/emoji/local/new-emoji.tsx @@ -23,7 +23,7 @@ import useShortcode from "./use-shortcode"; import useFormSubmit from "../../../../lib/form/submit"; import { TextInput, FileInput } from "../../../../components/form/inputs"; import { CategorySelect } from '../category-select'; -import FakeToot from "../../../../components/fake-toot"; +import FakeStatus from "../../../../components/status"; import MutationButton from "../../../../components/form/mutation-button"; import { useAddEmojiMutation } from "../../../../lib/query/admin/custom-emoji"; import { useInstanceV1Query } from "../../../../lib/query/gts-api"; @@ -103,9 +103,9 @@ export default function NewEmojiForm() {

Add new custom emoji

- + Look at this new custom emoji {emojiOrShortcode} isn't it cool? - +
{ // When clicking on a header perm, // go to the detail view for perm. diff --git a/web/source/settings/views/moderation/accounts/detail/index.tsx b/web/source/settings/views/moderation/accounts/detail/index.tsx index 830a894cbf..958a3121ba 100644 --- a/web/source/settings/views/moderation/accounts/detail/index.tsx +++ b/web/source/settings/views/moderation/accounts/detail/index.tsx @@ -21,13 +21,13 @@ import React from "react"; import { useGetAccountQuery } from "../../../../lib/query/admin"; import FormWithData from "../../../../lib/form/form-with-data"; -import FakeProfile from "../../../../components/fake-profile"; +import FakeProfile from "../../../../components/profile"; import { AdminAccount } from "../../../../lib/types/account"; import { AccountActions } from "./actions"; import { useParams } from "wouter"; import { useBaseUrl } from "../../../../lib/navigation/util"; import BackButton from "../../../../components/back-button"; -import { UseOurInstanceAccount, yesOrNo } from "./util"; +import { UseOurInstanceAccount, yesOrNo } from "../../../../lib/util"; export default function AccountDetail() { const params: { accountID: string } = useParams(); diff --git a/web/source/settings/views/moderation/accounts/search/index.tsx b/web/source/settings/views/moderation/accounts/search/index.tsx index 16e89ce435..f37e22a660 100644 --- a/web/source/settings/views/moderation/accounts/search/index.tsx +++ b/web/source/settings/views/moderation/accounts/search/index.tsx @@ -83,7 +83,7 @@ export function AccountSearchForm() { } // Location to return to when user clicks "back" on the account detail view. - const backLocation = location + (urlQueryParams ? `?${urlQueryParams}` : ""); + const backLocation = location + (urlQueryParams.size > 0 ? `?${urlQueryParams}` : ""); // Function to map an item to a list entry. function itemToEntry(account: AdminAccount): ReactNode { diff --git a/web/source/settings/views/moderation/reports/detail.tsx b/web/source/settings/views/moderation/reports/detail.tsx index ad8d69a47b..71abfea532 100644 --- a/web/source/settings/views/moderation/reports/detail.tsx +++ b/web/source/settings/views/moderation/reports/detail.tsx @@ -18,7 +18,7 @@ */ import React, { useState } from "react"; -import { useParams } from "wouter"; +import { useLocation, useParams } from "wouter"; import FormWithData from "../../../lib/form/form-with-data"; import BackButton from "../../../components/back-button"; import { useValue, useTextInput } from "../../../lib/form"; @@ -28,85 +28,147 @@ import MutationButton from "../../../components/form/mutation-button"; import Username from "../../../components/username"; import { useGetReportQuery, useResolveReportMutation } from "../../../lib/query/admin/reports"; import { useBaseUrl } from "../../../lib/navigation/util"; +import { AdminReport } from "../../../lib/types/report"; export default function ReportDetail({ }) { + const params: { reportId: string } = useParams(); const baseUrl = useBaseUrl(); - const params = useParams(); + const backLocation: String = history.state?.backLocation ?? `~${baseUrl}`; return ( -
-

Report Details

+
+

Report Details

); } -function ReportDetailForm({ data: report }) { +function ReportDetailForm({ data: report }: { data: AdminReport }) { + const [ location ] = useLocation(); + const baseUrl = useBaseUrl(); + const from = report.account; const target = report.target_account; + const comment = report.comment; + const status = report.action_taken ? "Resolved" : "Unresolved"; + const created = new Date(report.created_at).toLocaleString(); return ( -
-
- - <> reported - +
+
+
Target
+
+ +
+
+ +
+
Reported by
+
+ +
- {report.action_taken && -
-

Resolved by @{report.action_taken_by_account.account.acct}

- at {new Date(report.action_taken_at).toLocaleString()} -
- Comment: {report.action_taken_comment} -
- } - -
-

Report info:

-
- Created: - {new Date(report.created_at).toLocaleString()} - - Forwarded: {report.forwarded ? "Yes" : "No"} - Category: {report.category} +
+
Status
+
+ { report.action_taken + ? <>{status} + : {status} + } +
+
- Reason: - {report.comment.length > 0 - ?

{report.comment}

- : none provided +
+
Reason
+
+ { comment.length > 0 + ? <>{comment} + : none provided } +
+
-
+
+
Created
+
+ +
+
+ ); - {!report.action_taken && } + // return ( + // <> + //
+ // + // <> reported + // + //
- { - report.statuses.length > 0 && -
-

Reported toots ({report.statuses.length}):

-
- {report.statuses.map((status) => ( - - ))} -
-
- } -
- ); + // {report.action_taken && + //
+ //

Resolved by @{report.action_taken_by_account.account.acct}

+ // at {new Date(report.action_taken_at).toLocaleString()} + //
+ // Comment: {report.action_taken_comment} + //
+ // } + + //
+ //

Report info:

+ //
+ // Created: + // {new Date(report.created_at).toLocaleString()} + + // Forwarded: {report.forwarded ? "Yes" : "No"} + // Category: {report.category} + + // Reason: + // {report.comment.length > 0 + // ?

{report.comment}

+ // : none provided + // } + + //
+ //
+ + // {!report.action_taken && } + + // { + // report.statuses.length > 0 && + //
+ //

Reported toots ({report.statuses.length}):

+ //
+ // {report.statuses.map((status) => ( + // + // ))} + //
+ //
+ // } + // + // ); } function ReportActionForm({ report }) { diff --git a/web/source/settings/views/moderation/reports/search.tsx b/web/source/settings/views/moderation/reports/search.tsx index a78af847c2..b0952edf30 100644 --- a/web/source/settings/views/moderation/reports/search.tsx +++ b/web/source/settings/views/moderation/reports/search.tsx @@ -24,7 +24,7 @@ import { useTextInput } from "../../../lib/form"; import { PageableList } from "../../../components/pageable-list"; import { Select } from "../../../components/form/inputs"; import MutationButton from "../../../components/form/mutation-button"; -import { Link, useLocation, useSearch } from "wouter"; +import { useLocation, useSearch } from "wouter"; import Username from "../../../components/username"; import { AdminReport } from "../../../lib/types/report"; @@ -45,17 +45,27 @@ function ReportSearchForm() { const [ location, setLocation ] = useLocation(); const search = useSearch(); const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]); + const hasParams = urlQueryParams.size != 0; const [ searchReports, searchRes ] = useLazySearchReportsQuery(); // Populate search form using values from // urlQueryParams, to allow paging. + const resolved = useMemo(() => { + const resolvedRaw = urlQueryParams.get("resolved"); + if (resolvedRaw !== null) { + return resolvedRaw; + } + }, [urlQueryParams]); + const form = { - resolved: useTextInput("resolved", { defaultValue: urlQueryParams.get("resolved") ?? "false" }), + resolved: useTextInput("resolved", { defaultValue: resolved }), account_id: useTextInput("account_id", { defaultValue: urlQueryParams.get("account_id") ?? "" }), target_account_id: useTextInput("target_account_id", { defaultValue: urlQueryParams.get("target_account_id") ?? "" }), limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "20" }) }; + const setResolved = form.resolved.setter; + // On mount, if urlQueryParams were provided, // trigger the search. For example, if page // was accessed at /search?origin=local&limit=20, @@ -65,12 +75,20 @@ function ReportSearchForm() { // If no urlQueryParams set, use the default // search (just show unresolved reports). useEffect(() => { - if (urlQueryParams.size > 0) { + if (hasParams) { searchReports(Object.fromEntries(urlQueryParams), true); } else { - searchReports({resolved: false}, true); + setResolved("false"); + setLocation(location + "?resolved=false"); } - }, [urlQueryParams, searchReports]); + }, [ + urlQueryParams, + hasParams, + searchReports, + location, + setLocation, + setResolved, + ]); // Rather than triggering the search directly, // the "submit" button changes the location @@ -82,7 +100,7 @@ function ReportSearchForm() { // Parse query parameters. const entries = Object.entries(form).map(([k, v]) => { // Take only defined form fields. - if (v.value === undefined || v.value.length === 0) { + if (v.value === undefined || v.value.length === 0 || v.value === "any") { return null; } return [[k, v.value]]; @@ -96,13 +114,15 @@ function ReportSearchForm() { } // Location to return to when user clicks "back" on the detail view. - const backLocation = location + (urlQueryParams ? `?${urlQueryParams}` : ""); + const backLocation = location + (hasParams ? `?${urlQueryParams}` : ""); // Function to map an item to a list entry. function itemToEntry(report: AdminReport): ReactNode { return ( - ); @@ -150,24 +170,37 @@ function ReportSearchForm() { interface ReportEntryProps { report: AdminReport; - linkTo?: string; - backLocation?: string; + linkTo: string; + backLocation: string; } -function ReportEntry({ report }: ReportEntryProps) { +function ReportListEntry({ report, linkTo, backLocation }: ReportEntryProps) { + const [ _location, setLocation ] = useLocation(); + const from = report.account; const target = report.target_account; const comment = report.comment; const status = report.action_taken ? "Resolved" : "Unresolved"; const created = new Date(report.created_at).toLocaleString(); - const title = `${status}. @${from.account.acct} reported @${target.account.acct} on ${created}. Reason: "${comment}"`; + const title = `${status}. @${target.account.acct} was reported by @${from.account.acct} on ${created}. Reason: "${comment}"`; return ( - { + // When clicking on a report, direct + // to the detail view for that report. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }} + role="link" + tabIndex={0} >
@@ -214,6 +247,6 @@ function ReportEntry({ report }: ReportEntryProps) {
- + ); } diff --git a/web/source/settings/views/user/profile.tsx b/web/source/settings/views/user/profile.tsx index c1735259ef..a65405faac 100644 --- a/web/source/settings/views/user/profile.tsx +++ b/web/source/settings/views/user/profile.tsx @@ -39,7 +39,7 @@ import { } from "../../components/form/inputs"; import FormWithData from "../../lib/form/form-with-data"; -import FakeProfile from "../../components/fake-profile"; +import FakeProfile from "../../components/profile"; import MutationButton from "../../components/form/mutation-button"; import { useAccountThemesQuery } from "../../lib/query/user"; From 90ac61d77806a090b8e8bffef0d900f78cc2bb08 Mon Sep 17 00:00:00 2001 From: tobi Date: Tue, 18 Jun 2024 16:24:29 +0200 Subject: [PATCH 6/8] last bits --- web/source/settings/components/status.tsx | 179 ++++++++++- web/source/settings/lib/types/report.ts | 3 +- web/source/settings/lib/types/status.ts | 83 +++++ web/source/settings/style.css | 50 +-- .../views/admin/emoji/local/detail.tsx | 2 +- .../views/admin/emoji/local/new-emoji.tsx | 2 +- .../views/moderation/reports/detail.tsx | 285 +++++++----------- .../views/moderation/reports/search.tsx | 12 +- 8 files changed, 398 insertions(+), 218 deletions(-) create mode 100644 web/source/settings/lib/types/status.ts diff --git a/web/source/settings/components/status.tsx b/web/source/settings/components/status.tsx index d2d8106d01..efb9574d04 100644 --- a/web/source/settings/components/status.tsx +++ b/web/source/settings/components/status.tsx @@ -19,8 +19,9 @@ import React from "react"; import { useVerifyCredentialsQuery } from "../lib/query/oauth"; +import { MediaAttachment, Status as StatusType } from "../lib/types/status"; -export default function FakeStatus({ children }) { +export function FakeStatus({ children }) { const { data: account = { avatar: "/assets/default_avatars/GoToSocial_icon1.png", display_name: "", @@ -54,3 +55,179 @@ export default function FakeStatus({ children }) { ); } + +export function Status({ status }: { status: StatusType }) { + return ( + + ); +} + +function StatusHeader({ status }: { status: StatusType }) { + const author = status.account; + + return ( +
+
+ +
+
+ ); +} + +function StatusBody({ status }: { status: StatusType }) { + return ( +
+
+ + + { status.spoiler_text + ? status.spoiler_text + " " + : "[no content warning set] " + } + + + Toggle content visibility + + +
+ { status.content + ? status.content + : "[no content set] " + } +
+
+ +
+ ); +} + +function StatusMedia({ status }: { status: StatusType }) { + if (status.media_attachments.length === 0) { + return null; + } + + const count = status.media_attachments.length; + const aria_label = count === 1 ? "1 attachment" : `${count} attachments`; + const oddOrEven = count % 2 === 0 ? "even" : "odd"; + const single = count === 1 ? " single" : ""; + + return ( +
+ { status.media_attachments.map((media) => { + return ( + + ); + })} +
+ ); +} + +function StatusMediaEntry({ media }: { media: MediaAttachment }) { + return ( +
+
+ + + + + + + {media.description} + + + {media.description} + +
+
+ ); +} + +function StatusFooter({ status }: { status: StatusType }) { + return ( + + ); +} diff --git a/web/source/settings/lib/types/report.ts b/web/source/settings/lib/types/report.ts index 2c663e7031..4ef694be68 100644 --- a/web/source/settings/lib/types/report.ts +++ b/web/source/settings/lib/types/report.ts @@ -19,6 +19,7 @@ import { Links } from "parse-link-header"; import { AdminAccount } from "./account"; +import { Status } from "./status"; /** * Admin model of a report. Differs from the client @@ -77,7 +78,7 @@ export interface AdminReport { * Statuses cited by this report, if any. * TODO: model this properly. */ - statuses: Object[]; + statuses: Status[]; /** * Rules broken according to the reporter, if any. * TODO: model this properly. diff --git a/web/source/settings/lib/types/status.ts b/web/source/settings/lib/types/status.ts new file mode 100644 index 0000000000..e46f4a6b7a --- /dev/null +++ b/web/source/settings/lib/types/status.ts @@ -0,0 +1,83 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + 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 . +*/ + +import { Account } from "./account"; +import { CustomEmoji } from "./custom-emoji"; + +export interface Status { + id: string; + created_at: string; + in_reply_to_id: string | null; + in_reply_to_account_id: string | null; + sensitive: boolean; + spoiler_text: string; + visibility: string; + language: string; + uri: string; + url: string; + replies_count: number; + reblogs_count: number; + favourites_count: number; + favourited: boolean; + reblogged: boolean; + muted: boolean; + bookmarked: boolean; + pinned: boolean; + content: string, + reblog: Status | null, + account: Account, + media_attachments: MediaAttachment[], + mentions: []; + tags: []; + emojis: CustomEmoji[]; + card: null; + poll: null; +} + +export interface MediaAttachment { + id: string; + type: string; + url: string; + text_url: string; + preview_url: string; + remote_url: string | null; + preview_remote_url: string | null; + meta: MediaAttachmentMeta; + description: string; + blurhash: string; +} + +interface MediaAttachmentMeta { + original: { + width: number; + height: number; + size: string; + aspect: number; + }, + small: { + width: number; + height: number; + size: string; + aspect: number; + }, + focus: { + x: number; + y: number; + } +} diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 17f21bbf2d..cdae6b9722 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -1109,45 +1109,14 @@ button.with-padding { padding: 0; } } - - .report.detail { - display: flex; - flex-direction: column; - margin-top: 1rem; - gap: 1rem; - - .info-block { - padding: 0.5rem; - background: $gray2; - } - - .info { - display: block; - } - - .reported-toots { - margin-top: 0.5rem; - } - - .toot .toot-info { - padding: 0.5rem; - background: $toot-info-bg; - - a { - color: $fg-reduced; - } - - &:last-child { - border-bottom-left-radius: $br; - border-bottom-right-radius: $br; - } - } - } } .report-detail { .info-list { - margin-top: 1rem; + + &.overview { + margin-top: 1rem; + } .username-lozenge { display: flex; @@ -1163,6 +1132,17 @@ button.with-padding { } } } + + .report-statuses { + width: min(100%, 50rem); + + .thread { + display: flex; + flex-direction: column; + gap: 2rem; + padding: 0; + } + } } .accounts-view { diff --git a/web/source/settings/views/admin/emoji/local/detail.tsx b/web/source/settings/views/admin/emoji/local/detail.tsx index bb46e8afeb..4126bbedc2 100644 --- a/web/source/settings/views/admin/emoji/local/detail.tsx +++ b/web/source/settings/views/admin/emoji/local/detail.tsx @@ -22,7 +22,7 @@ import { Redirect, useParams } from "wouter"; import { useComboBoxInput, useFileInput, useValue } from "../../../../lib/form"; import useFormSubmit from "../../../../lib/form/submit"; import { useBaseUrl } from "../../../../lib/navigation/util"; -import FakeStatus from "../../../../components/status"; +import { FakeStatus } from "../../../../components/status"; import FormWithData from "../../../../lib/form/form-with-data"; import Loading from "../../../../components/loading"; import { FileInput } from "../../../../components/form/inputs"; diff --git a/web/source/settings/views/admin/emoji/local/new-emoji.tsx b/web/source/settings/views/admin/emoji/local/new-emoji.tsx index 99035e9bf7..f2f5a56b15 100644 --- a/web/source/settings/views/admin/emoji/local/new-emoji.tsx +++ b/web/source/settings/views/admin/emoji/local/new-emoji.tsx @@ -23,7 +23,7 @@ import useShortcode from "./use-shortcode"; import useFormSubmit from "../../../../lib/form/submit"; import { TextInput, FileInput } from "../../../../components/form/inputs"; import { CategorySelect } from '../category-select'; -import FakeStatus from "../../../../components/status"; +import { FakeStatus } from "../../../../components/status"; import MutationButton from "../../../../components/form/mutation-button"; import { useAddEmojiMutation } from "../../../../lib/query/admin/custom-emoji"; import { useInstanceV1Query } from "../../../../lib/query/gts-api"; diff --git a/web/source/settings/views/moderation/reports/detail.tsx b/web/source/settings/views/moderation/reports/detail.tsx index 71abfea532..7d6e542fbd 100644 --- a/web/source/settings/views/moderation/reports/detail.tsx +++ b/web/source/settings/views/moderation/reports/detail.tsx @@ -17,7 +17,7 @@ along with this program. If not, see . */ -import React, { useState } from "react"; +import React from "react"; import { useLocation, useParams } from "wouter"; import FormWithData from "../../../lib/form/form-with-data"; import BackButton from "../../../components/back-button"; @@ -29,6 +29,8 @@ import Username from "../../../components/username"; import { useGetReportQuery, useResolveReportMutation } from "../../../lib/query/admin/reports"; import { useBaseUrl } from "../../../lib/navigation/util"; import { AdminReport } from "../../../lib/types/report"; +import { yesOrNo } from "../../../lib/util"; +import { Status } from "../../../components/status"; export default function ReportDetail({ }) { const params: { reportId: string } = useParams(); @@ -52,6 +54,40 @@ function ReportDetailForm({ data: report }: { data: AdminReport }) { const [ location ] = useLocation(); const baseUrl = useBaseUrl(); + return ( + <> + + + { report.action_taken + && + } + + { report.statuses && + + } + + { !report.action_taken && + + } + + ); +} + +interface ReportSectionProps { + report: AdminReport; + baseUrl: string; + location: string; +} + +function ReportBasicInfo({ report, baseUrl, location }: ReportSectionProps) { const from = report.account; const target = report.target_account; const comment = report.comment; @@ -59,9 +95,9 @@ function ReportDetailForm({ data: report }: { data: AdminReport }) { const created = new Date(report.created_at).toLocaleString(); return ( -
+
-
Target
+
Reported account
{created}
-
- ); - - // return ( - // <> - //
- // - // <> reported - // - //
- - // {report.action_taken && - //
- //

Resolved by @{report.action_taken_by_account.account.acct}

- // at {new Date(report.action_taken_at).toLocaleString()} - //
- // Comment: {report.action_taken_comment} - //
- // } - //
- //

Report info:

- //
- // Created: - // {new Date(report.created_at).toLocaleString()} - - // Forwarded: {report.forwarded ? "Yes" : "No"} - // Category: {report.category} +
+
Category
+
{ report.category }
+
- // Reason: - // {report.comment.length > 0 - // ?

{report.comment}

- // : none provided - // } +
+
Forwarded
+
{ yesOrNo(report.forwarded) }
+
+
+ ); +} - //
- //
+function ReportHistory({ report, baseUrl, location }: ReportSectionProps) { + const handled_by = report.action_taken_by_account; + if (!handled_by) { + throw "report handled by action_taken_by_account undefined"; + } + + const handled = report.action_taken_at ? new Date(report.action_taken_at).toLocaleString() : "never"; + + return ( + <> +

Moderation History

+
+
+
Handled by
+
+ +
+
- // {!report.action_taken && } +
+
Handled
+
+ +
+
- // { - // report.statuses.length > 0 && - //
- //

Reported toots ({report.statuses.length}):

- //
- // {report.statuses.map((status) => ( - // - // ))} - //
- //
- // } - // - // ); +
+
Comment
+
{ report.action_taken_comment ?? "none"}
+
+
+ + ); } function ReportActionForm({ report }) { @@ -180,13 +206,18 @@ function ReportActionForm({ report }) { const [submit, result] = useFormSubmit(form, useResolveReportMutation(), { changedOnly: false }); return ( - -

Resolving this report

-

+ +

Resolve this report

+ <> An optional comment can be included while resolving 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! -

+ This is useful for providing an explanation about what action was + taken (if any) before the report was marked as resolved. +
+ + Any comment made here will be visible + to the user that created the report! + +