[feature] Instance rules (#2125)

* init instance rules database model, admin api

* expose instance rules in public instance api

* public /api/v1/instance/rules route

* GET ruleById

* createRule route

* createRule auth check

* updateRule

* deleteRule

* list rules on about page

* ruleGet auth

* add about page ids for anchors

* process and store adding violated rules to reports

* admin api models for instance rules

* instance rule edit frontend

* change rule inputs to textareas

* database fixes after rebase (#2124)

* remove unused imports

* fix db migration column name

* fix tests

* fix more tests

* fix postgres error with wrongly used Ident

* add some tests, fiddle with rule model a bit, fix postgres migration

* swagger docs

---------

Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
This commit is contained in:
f0x52 2023-08-19 14:33:15 +02:00 committed by GitHub
commit 92de8fb396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2189 additions and 107 deletions

View file

@ -25,22 +25,24 @@ import (
)
const (
BasePath = "/v1/admin"
EmojiPath = BasePath + "/custom_emojis"
EmojiPathWithID = EmojiPath + "/:" + IDKey
EmojiCategoriesPath = EmojiPath + "/categories"
DomainBlocksPath = BasePath + "/domain_blocks"
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
AccountsPath = BasePath + "/accounts"
AccountsPathWithID = AccountsPath + "/:" + IDKey
AccountsActionPath = AccountsPathWithID + "/action"
MediaCleanupPath = BasePath + "/media_cleanup"
MediaRefetchPath = BasePath + "/media_refetch"
ReportsPath = BasePath + "/reports"
ReportsPathWithID = ReportsPath + "/:" + IDKey
ReportsResolvePath = ReportsPathWithID + "/resolve"
EmailPath = BasePath + "/email"
EmailTestPath = EmailPath + "/test"
BasePath = "/v1/admin"
EmojiPath = BasePath + "/custom_emojis"
EmojiPathWithID = EmojiPath + "/:" + IDKey
EmojiCategoriesPath = EmojiPath + "/categories"
DomainBlocksPath = BasePath + "/domain_blocks"
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
AccountsPath = BasePath + "/accounts"
AccountsPathWithID = AccountsPath + "/:" + IDKey
AccountsActionPath = AccountsPathWithID + "/action"
MediaCleanupPath = BasePath + "/media_cleanup"
MediaRefetchPath = BasePath + "/media_refetch"
ReportsPath = BasePath + "/reports"
ReportsPathWithID = ReportsPath + "/:" + IDKey
ReportsResolvePath = ReportsPathWithID + "/resolve"
EmailPath = BasePath + "/email"
EmailTestPath = EmailPath + "/test"
InstanceRulesPath = BasePath + "/instance/rules"
InstanceRulesPathWithID = InstanceRulesPath + "/:" + IDKey
IDKey = "id"
FilterQueryKey = "filter"
@ -95,4 +97,11 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
// email stuff
attachHandler(http.MethodPost, EmailTestPath, m.EmailTestPOSTHandler)
// instance rules stuff
attachHandler(http.MethodGet, InstanceRulesPath, m.RulesGETHandler)
attachHandler(http.MethodGet, InstanceRulesPathWithID, m.RuleGETHandler)
attachHandler(http.MethodPost, InstanceRulesPath, m.RulePOSTHandler)
attachHandler(http.MethodPatch, InstanceRulesPathWithID, m.RulePATCHHandler)
attachHandler(http.MethodDelete, InstanceRulesPathWithID, m.RuleDELETEHandler)
}

View file

@ -335,7 +335,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},
"statuses": [],
"rule_ids": [],
"rules": [],
"action_taken_comment": "user was warned not to be a turtle anymore"
},
{
@ -528,7 +528,16 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
"poll": null
}
],
"rule_ids": [],
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
],
"action_taken_comment": null
}
]`, string(b))
@ -740,7 +749,16 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
"poll": null
}
],
"rule_ids": [],
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
],
"action_taken_comment": null
}
]`, string(b))
@ -952,7 +970,16 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
"poll": null
}
],
"rule_ids": [],
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
],
"action_taken_comment": null
}
]`, string(b))

View file

@ -0,0 +1,120 @@
// 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 <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// RulePOSTHandler swagger:operation POST /api/v1/admin/instance/rules ruleCreate
//
// Create a new instance rule.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
//
// produces:
// - application/json
//
// parameters:
// -
// name: text
// in: formData
// description: >-
// Text body for the instance rule, plaintext.
// type: string
// required: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The newly-created instance rule.
// schema:
// "$ref": "#/definitions/instanceRule"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) RulePOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
form := &apimodel.InstanceRuleCreateRequest{}
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
if err := validateCreateRule(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
apiRule, errWithCode := m.processor.Admin().RuleCreate(c.Request.Context(), form)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
c.JSON(http.StatusOK, apiRule)
}
func validateCreateRule(form *apimodel.InstanceRuleCreateRequest) error {
if form.Text == "" {
return errors.New("Instance rule text is empty")
}
return nil
}

View file

@ -0,0 +1,107 @@
// 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 <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// RuleDELETEHandler swagger:operation DELETE /api/v1/admin/instance/rules{id} ruleDelete
//
// Delete an existing instance rule.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// in: formData
// description: >-
// The id of the rule to delete.
// type: path
// required: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The deleted instance rule.
// schema:
// "$ref": "#/definitions/instanceRule"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) RuleDELETEHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
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)
return
}
apiRule, errWithCode := m.processor.Admin().RuleDelete(c.Request.Context(), ruleID)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
c.JSON(http.StatusOK, apiRule)
}

View file

@ -0,0 +1,102 @@
// 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 <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// RuleGETHandler swagger:operation GET /api/v1/admin/rules/{id} adminRuleGet
//
// View instance rule with the given id.
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// type: string
// description: The id of the rule.
// in: path
// required: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// name: rule
// description: The requested rule.
// schema:
// "$ref": "#/definitions/instanceRule"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) RuleGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
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)
return
}
rule, errWithCode := m.processor.Admin().RuleGet(c.Request.Context(), ruleID)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
c.JSON(http.StatusOK, rule)
}

View file

@ -0,0 +1,91 @@
// 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 <http://www.gnu.org/licenses/>.
package admin
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// rulesGETHandler swagger:operation GET /api/v1/admin/rules rules
//
// View instance rules, with IDs.
//
// The rules will be returned in order (sorted by Order ascending).
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: An array with all the rules for the local instance.
// schema:
// type: array
// items:
// "$ref": "#/definitions/instanceRule"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) RulesGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
resp, errWithCode := m.processor.Admin().RulesGet(c.Request.Context())
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
c.JSON(http.StatusOK, resp)
}

View file

@ -0,0 +1,127 @@
// 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 <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// RulePATCHHandler swagger:operation PATCH /api/v1/admin/instance/rules{id} ruleUpdate
//
// Update an existing instance rule.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// in: formData
// description: >-
// The id of the rule to update.
// type: path
// required: true
// -
// name: text
// in: formData
// description: >-
// Text body for the updated instance rule, plaintext.
// type: string
// required: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The updated instance rule.
// schema:
// "$ref": "#/definitions/instanceRule"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) RulePATCHHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
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)
return
}
form := &apimodel.InstanceRuleCreateRequest{}
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
// reuses CreateRule validator
if err := validateCreateRule(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
apiRule, errWithCode := m.processor.Admin().RuleUpdate(c.Request.Context(), ruleID, form)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
c.JSON(http.StatusOK, apiRule)
}

View file

@ -28,6 +28,7 @@ const (
InstanceInformationPathV1 = "/v1/instance"
InstanceInformationPathV2 = "/v2/instance"
InstancePeersPath = InstanceInformationPathV1 + "/peers"
InstanceRulesPath = InstanceInformationPathV1 + "/rules"
PeersFilterKey = "filter" // PeersFilterKey is used to provide filters to /api/v1/instance/peers
)
@ -47,4 +48,6 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
attachHandler(http.MethodPatch, InstanceInformationPathV1, m.InstanceUpdatePATCHHandler)
attachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler)
attachHandler(http.MethodGet, InstanceRulesPath, m.InstanceRulesGETHandler)
}

View file

@ -160,7 +160,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
"name": "admin"
}
},
"max_toot_chars": 5000
"max_toot_chars": 5000,
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
]
}`, dst.String())
}
@ -264,7 +274,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
"name": "admin"
}
},
"max_toot_chars": 5000
"max_toot_chars": 5000,
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
]
}`, dst.String())
}
@ -368,7 +388,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
"name": "admin"
}
},
"max_toot_chars": 5000
"max_toot_chars": 5000,
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
]
}`, dst.String())
}
@ -523,7 +553,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
"name": "admin"
}
},
"max_toot_chars": 5000
"max_toot_chars": 5000,
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
]
}`, dst.String())
}
@ -651,7 +691,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
"name": "admin"
}
},
"max_toot_chars": 5000
"max_toot_chars": 5000,
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
]
}`, dst.String())
// extra bonus: check the v2 model thumbnail after the patch
@ -790,7 +840,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
"name": "admin"
}
},
"max_toot_chars": 5000
"max_toot_chars": 5000,
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
]
}`, dst.String())
}

View file

@ -0,0 +1,71 @@
// 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 <http://www.gnu.org/licenses/>.
package instance
import (
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// instanceRulesGETHandler swagger:operation GET /api/v1/instance/rules rules
//
// View instance rules (public).
//
// The rules will be returned in order (sorted by Order ascending).
//
// ---
// tags:
// - instance
//
// produces:
// - application/json
//
// parameters:
//
// responses:
// '200':
// description: An array with all the rules for the local instance.
// schema:
// type: array
// items:
// "$ref": "#/definitions/instanceRule"
// '400':
// description: bad request
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) InstanceRulesGETHandler(c *gin.Context) {
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
resp, errWithCode := m.processor.InstanceGetRules(c.Request.Context())
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
c.JSON(http.StatusOK, resp)
}

View file

@ -51,17 +51,13 @@ func (suite *ReportCreateTestSuite) createReport(expectedHTTPStatus int, expecte
// create the request
ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+reports.BasePath, nil)
ctx.Request.Header.Set("accept", "application/json")
ruleIDs := make([]string, 0, len(form.RuleIDs))
for _, r := range form.RuleIDs {
ruleIDs = append(ruleIDs, strconv.Itoa(r))
}
ctx.Request.Form = url.Values{
"account_id": {form.AccountID},
"status_ids[]": form.StatusIDs,
"comment": {form.Comment},
"forward": {strconv.FormatBool(form.Forward)},
"category": {form.Category},
"rule_ids[]": ruleIDs,
"rule_ids[]": form.RuleIDs,
}
// trigger the handler

View file

@ -108,7 +108,10 @@ func (suite *ReportGetTestSuite) TestGetReport1() {
"status_ids": [
"01FVW7JHQFSFK166WWKR8CBA6M"
],
"rule_ids": [],
"rule_ids": [
"01GP3AWY4CRDVRNZKW0TEAMB51",
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
],
"target_account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",

View file

@ -133,7 +133,10 @@ func (suite *ReportsGetTestSuite) TestGetReports() {
"status_ids": [
"01FVW7JHQFSFK166WWKR8CBA6M"
],
"rule_ids": [],
"rule_ids": [
"01GP3AWY4CRDVRNZKW0TEAMB51",
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
],
"target_account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",
@ -220,7 +223,10 @@ func (suite *ReportsGetTestSuite) TestGetReports4() {
"status_ids": [
"01FVW7JHQFSFK166WWKR8CBA6M"
],
"rule_ids": [],
"rule_ids": [
"01GP3AWY4CRDVRNZKW0TEAMB51",
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
],
"target_account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",
@ -291,7 +297,10 @@ func (suite *ReportsGetTestSuite) TestGetReports6() {
"status_ids": [
"01FVW7JHQFSFK166WWKR8CBA6M"
],
"rule_ids": [],
"rule_ids": [
"01GP3AWY4CRDVRNZKW0TEAMB51",
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
],
"target_account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",
@ -346,7 +355,10 @@ func (suite *ReportsGetTestSuite) TestGetReports7() {
"status_ids": [
"01FVW7JHQFSFK166WWKR8CBA6M"
],
"rule_ids": [],
"rule_ids": [
"01GP3AWY4CRDVRNZKW0TEAMB51",
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
],
"target_account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",

View file

@ -117,9 +117,9 @@ type AdminReport struct {
// Array of statuses that were submitted along with this report.
// Will be empty if no status IDs were submitted with the report.
Statuses []*Status `json:"statuses"`
// Array of rule IDs that were submitted along with this report.
// NOT IMPLEMENTED, will always be empty array.
Rules []interface{} `json:"rule_ids"`
// Array of rules that were broken according to this report.
// Will be empty if no rule IDs were submitted with the report.
Rules []*InstanceRule `json:"rules"`
// If an action was taken, what comment was made by the admin on the taken action?
// Will be null if not set / no action yet taken.
// example: Account was suspended.
@ -189,3 +189,10 @@ type AdminSendTestEmailRequest struct {
// Email address to send the test email to.
Email string `form:"email" json:"email" xml:"email"`
}
type AdminInstanceRule struct {
ID string `json:"id"` // id of this item in the database
CreatedAt string `json:"created_at"` // when was item created
UpdatedAt string `json:"updated_at"` // when was item last updated
Text string `json:"text"` // text content of the rule
}

View file

@ -88,6 +88,8 @@ type InstanceV1 struct {
//
// example: 5000
MaxTootChars uint `json:"max_toot_chars"`
// An itemized list of rules for this instance.
Rules []InstanceRule `json:"rules"`
}
// InstanceV1URLs models instance-relevant URLs for client application consumption.

View file

@ -62,9 +62,8 @@ type InstanceV2 struct {
Registrations InstanceV2Registrations `json:"registrations"`
// Hints related to contacting a representative of the instance.
Contact InstanceV2Contact `json:"contact"`
// An itemized list of rules for this website.
// Currently not implemented (will always be empty array).
Rules []interface{} `json:"rules"`
// An itemized list of rules for this instance.
Rules []InstanceRule `json:"rules"`
}
// Usage data for this instance.

View file

@ -54,8 +54,8 @@ type Report struct {
StatusIDs []string `json:"status_ids"`
// Array of rule IDs that were submitted along with this report.
// Will be empty if no rule IDs were submitted.
// example: [1, 2]
RuleIDs []int `json:"rule_ids"`
// example: ["01GPBN5YDY6JKBWE44H7YQBDCQ","01GPBN65PDWSBPWVDD0SQCFFY3"]
RuleIDs []string `json:"rule_ids"`
// Account that was reported.
TargetAccount *Account `json:"target_account"`
}
@ -89,8 +89,7 @@ type ReportCreateRequest struct {
// in: formData
Category string `form:"category" json:"category" xml:"category"`
// IDs of rules on this instance which have been broken according to the reporter.
// This is currently not supported, provided only for API compatibility.
// example: [1, 2, 3]
// example: ["01GPBN5YDY6JKBWE44H7YQBDCQ","01GPBN65PDWSBPWVDD0SQCFFY3"]
// in: formData
RuleIDs []int `form:"rule_ids[]" json:"rule_ids" xml:"rule_ids"`
RuleIDs []string `form:"rule_ids[]" json:"rule_ids" xml:"rule_ids"`
}

View file

@ -0,0 +1,41 @@
// 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 <http://www.gnu.org/licenses/>.
package model
// InstanceRule represents a single instance rule.
//
// swagger:model instanceRule
type InstanceRule struct {
ID string `json:"id"`
Text string `json:"text"`
}
// InstanceRuleCreateRequest represents a request to create a new instance rule, made through the admin API.
//
// swagger:model instanceRuleCreateRequest
type InstanceRuleCreateRequest struct {
Text string `form:"text" validation:"required"`
}
// InstanceRuleUpdateRequest represents a request to update the text of an instance rule, made through the admin API.
//
// swagger:model instanceRuleUpdateRequest
type InstanceRuleUpdateRequest struct {
ID string `form:"id"`
Text string `form:"text"`
}

View file

@ -72,6 +72,7 @@ type DBService struct {
db.Notification
db.Relationship
db.Report
db.Rule
db.Search
db.Session
db.Status
@ -216,6 +217,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
db: db,
state: state,
},
Rule: &ruleDB{
db: db,
state: state,
},
Search: &searchDB{
db: db,
state: state,

View file

@ -51,6 +51,7 @@ type BunDBStandardTestSuite struct {
testListEntries map[string]*gtsmodel.ListEntry
testAccountNotes map[string]*gtsmodel.AccountNote
testMarkers map[string]*gtsmodel.Marker
testRules map[string]*gtsmodel.Rule
}
func (suite *BunDBStandardTestSuite) SetupSuite() {
@ -72,6 +73,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testListEntries = testrig.NewTestListEntries()
suite.testAccountNotes = testrig.NewTestAccountNotes()
suite.testMarkers = testrig.NewTestMarkers()
suite.testRules = testrig.NewTestRules()
}
func (suite *BunDBStandardTestSuite) SetupTest() {

View file

@ -151,6 +151,16 @@ func (i *instanceDB) getInstance(ctx context.Context, lookup string, dbQuery fun
return nil, err
}
if instance.Domain == config.GetHost() {
// also populate Rules
rules, err := i.state.DB.GetActiveRules(ctx)
if err != nil {
log.Error(ctx, err)
} else {
instance.Rules = rules
}
}
return &instance, nil
}, keyParts...)
if err != nil {

View file

@ -0,0 +1,47 @@
// 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 <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
if _, err := tx.NewCreateTable().Model(&gtsmodel.Rule{}).IfNotExists().Exec(ctx); err != nil {
return err
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -0,0 +1,53 @@
// 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 <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
"strings"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
if db.Dialect().Name() == dialect.SQLite { // sqlite does not have an array type
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? VARCHAR", bun.Ident("reports"), bun.Ident("rules"))
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
return err
}
} else {
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? VARCHAR[]", bun.Ident("reports"), bun.Ident("rules"))
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
return err
}
}
return nil
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -186,6 +186,19 @@ func (r *reportDB) PopulateReport(ctx context.Context, report *gtsmodel.Report)
}
}
if l := len(report.RuleIDs); l > 0 && l != len(report.Rules) {
// Report target rules not set, fetch from the database.
for _, v := range report.RuleIDs {
rule, err := r.state.DB.GetRuleByID(ctx, v)
if err != nil {
errs.Appendf("error populating report rules: %w", err)
} else {
report.Rules = append(report.Rules, rule)
}
}
}
if report.ActionTakenByAccountID != "" &&
report.ActionTakenByAccount == nil {
// Report action account is not set, fetch from the database.

149
internal/db/bundb/rule.go Normal file
View file

@ -0,0 +1,149 @@
// 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 <http://www.gnu.org/licenses/>.
package bundb
import (
"context"
"errors"
"time"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
)
type ruleDB struct {
db *DB
state *state.State
}
func (r *ruleDB) GetRuleByID(ctx context.Context, id string) (*gtsmodel.Rule, error) {
var rule gtsmodel.Rule
q := r.db.
NewSelect().
Model(&rule).
Where("? = ?", bun.Ident("rule.id"), id)
if err := q.Scan(ctx); err != nil {
return nil, err
}
return &rule, nil
}
func (r *ruleDB) GetRulesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Rule, error) {
rules := make([]*gtsmodel.Rule, 0, len(ids))
for _, id := range ids {
// Attempt to fetch status from DB.
rule, err := r.GetRuleByID(ctx, id)
if err != nil {
log.Errorf(ctx, "error getting rule %q: %v", id, err)
continue
}
// Append status to return slice.
rules = append(rules, rule)
}
return rules, nil
}
func (r *ruleDB) GetActiveRules(ctx context.Context) ([]gtsmodel.Rule, error) {
rules := make([]gtsmodel.Rule, 0)
q := r.db.
NewSelect().
Model(&rules).
// Ignore deleted (ie., inactive) rules.
Where("? = ?", bun.Ident("rule.deleted"), false).
Order("rule.order ASC")
if err := q.Scan(ctx); err != nil {
return nil, err
}
return rules, nil
}
func (r *ruleDB) PutRule(ctx context.Context, rule *gtsmodel.Rule) error {
var lastRuleOrder uint
// Select highest existing rule order.
err := r.db.
NewSelect().
TableExpr("? AS ?", bun.Ident("rules"), bun.Ident("rule")).
Column("rule.order").
Order("rule.order DESC").
Limit(1).
Scan(ctx, &lastRuleOrder)
switch {
case errors.Is(err, db.ErrNoEntries):
// No rules set yet, index from 0.
rule.Order = util.Ptr(uint(0))
case err != nil:
// Real db error.
return err
default:
// No error means previous rule(s)
// existed. New rule order should
// be 1 higher than previous rule.
rule.Order = func() *uint {
o := lastRuleOrder + 1
return &o
}()
}
if _, err := r.db.
NewInsert().
Model(rule).
Exec(ctx); err != nil {
return err
}
// invalidate cached local instance response, so it gets updated with the new rules
r.state.Caches.GTS.Instance().Invalidate("Domain", config.GetHost())
return nil
}
func (r *ruleDB) UpdateRule(ctx context.Context, rule *gtsmodel.Rule) (*gtsmodel.Rule, error) {
// Update the rule's last-updated
rule.UpdatedAt = time.Now()
if _, err := r.db.
NewUpdate().
Model(rule).
WherePK().
Exec(ctx); err != nil {
return nil, err
}
// invalidate cached local instance response, so it gets updated with the new rules
r.state.Caches.GTS.Instance().Invalidate("Domain", config.GetHost())
return rule, nil
}

View file

@ -0,0 +1,122 @@
// 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 <http://www.gnu.org/licenses/>.
package bundb_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
)
type RuleTestSuite struct {
BunDBStandardTestSuite
}
func (suite *RuleTestSuite) TestPutRuleWithExisting() {
r := &gtsmodel.Rule{
ID: id.NewULID(),
Text: "Pee pee poo poo",
}
if err := suite.state.DB.PutRule(context.Background(), r); err != nil {
suite.FailNow(err.Error())
}
suite.Equal(uint(len(suite.testRules)), *r.Order)
}
func (suite *RuleTestSuite) TestPutRuleNoExisting() {
var (
ctx = context.Background()
whereAny = []db.Where{{Key: "id", Value: "", Not: true}}
)
// Wipe all existing rules from the DB.
if err := suite.state.DB.DeleteWhere(
ctx,
whereAny,
&[]*gtsmodel.Rule{},
); err != nil {
suite.FailNow(err.Error())
}
r := &gtsmodel.Rule{
ID: id.NewULID(),
Text: "Pee pee poo poo",
}
if err := suite.state.DB.PutRule(ctx, r); err != nil {
suite.FailNow(err.Error())
}
// New rule is now only rule.
suite.EqualValues(uint(0), *r.Order)
}
func (suite *RuleTestSuite) TestGetRuleByID() {
rule, err := suite.state.DB.GetRuleByID(
context.Background(),
suite.testRules["rule1"].ID,
)
if err != nil {
suite.FailNow(err.Error())
}
suite.NotNil(rule)
}
func (suite *RuleTestSuite) TestGetRulesByID() {
ruleIDs := make([]string, 0, len(suite.testRules))
for _, rule := range suite.testRules {
ruleIDs = append(ruleIDs, rule.ID)
}
rules, err := suite.state.DB.GetRulesByIDs(
context.Background(),
ruleIDs,
)
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(rules, len(suite.testRules))
}
func (suite *RuleTestSuite) TestGetActiveRules() {
var activeRules int
for _, rule := range suite.testRules {
if !*rule.Deleted {
activeRules++
}
}
rules, err := suite.state.DB.GetActiveRules(context.Background())
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(rules, activeRules)
}
func TestRuleTestSuite(t *testing.T) {
suite.Run(t, new(RuleTestSuite))
}

View file

@ -38,6 +38,7 @@ type DB interface {
Notification
Relationship
Report
Rule
Search
Session
Status

42
internal/db/rule.go Normal file
View file

@ -0,0 +1,42 @@
// 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 <http://www.gnu.org/licenses/>.
package db
import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// Rule handles getting/creation/deletion/updating of instance rules.
type Rule interface {
// GetRuleByID gets one rule by its db id.
GetRuleByID(ctx context.Context, id string) (*gtsmodel.Rule, error)
// GetRulesByIDs gets multiple rules by their db idd.
GetRulesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Rule, error)
// GetRules gets all active (not deleted) rules.
GetActiveRules(ctx context.Context) ([]gtsmodel.Rule, error)
// PutRule puts the given rule in the database.
PutRule(ctx context.Context, rule *gtsmodel.Rule) error
// UpdateRule updates one rule by its db id.
UpdateRule(ctx context.Context, rule *gtsmodel.Rule) (*gtsmodel.Rule, error)
}

View file

@ -39,4 +39,5 @@ type Instance struct {
ContactAccount *Account `bun:"rel:belongs-to"` // account corresponding to contactAccountID
Reputation int64 `bun:",notnull,default:0"` // Reputation score of this instance
Version string `bun:",nullzero"` // Version of the software used on this instance
Rules []Rule `bun:"-"` // List of instance rules
}

View file

@ -37,6 +37,8 @@ type Report struct {
Comment string `bun:",nullzero"` // comment / explanation for this report, by the reporter
StatusIDs []string `bun:"statuses,array"` // database IDs of any statuses referenced by this report
Statuses []*Status `bun:"-"` // statuses corresponding to StatusIDs
RuleIDs []string `bun:"rules,array"` // database IDs of any rules referenced by this report
Rules []*Rule `bun:"-"` // rules corresponding to RuleIDs
Forwarded *bool `bun:",nullzero,notnull,default:false"` // flag to indicate report should be forwarded to remote instance
ActionTaken string `bun:",nullzero"` // string description of what action was taken in response to this report
ActionTakenAt time.Time `bun:"type:timestamptz,nullzero"` // time at which action was taken, if any

30
internal/gtsmodel/rule.go Normal file
View file

@ -0,0 +1,30 @@
// 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 <http://www.gnu.org/licenses/>.
package gtsmodel
import "time"
// Rule models an instance rule set by the admin
type Rule struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
Text string `bun:",nullzero"` // text content of the rule
Order *uint `bun:",nullzero,notnull,unique"` // rule ordering, index from 0
Deleted *bool `bun:",nullzero,notnull,default:false"` // has this rule been deleted, still kept in database for reference in historic reports
}

View file

@ -0,0 +1,127 @@
// 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 <http://www.gnu.org/licenses/>.
package admin
import (
"context"
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// RulesGet returns all rules stored on this instance.
func (p *Processor) RulesGet(
ctx context.Context,
) ([]*apimodel.AdminInstanceRule, gtserror.WithCode) {
rules, err := p.state.DB.GetActiveRules(ctx)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
apiRules := make([]*apimodel.AdminInstanceRule, len(rules))
for i := range rules {
apiRules[i] = p.tc.InstanceRuleToAdminAPIRule(&rules[i])
}
return apiRules, nil
}
// RuleGet returns one rule, with the given ID.
func (p *Processor) RuleGet(ctx context.Context, id string) (*apimodel.AdminInstanceRule, gtserror.WithCode) {
rule, err := p.state.DB.GetRuleByID(ctx, id)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(err)
}
return nil, gtserror.NewErrorInternalError(err)
}
return p.tc.InstanceRuleToAdminAPIRule(rule), nil
}
// RuleCreate adds a new rule to the instance.
func (p *Processor) RuleCreate(ctx context.Context, form *apimodel.InstanceRuleCreateRequest) (*apimodel.AdminInstanceRule, gtserror.WithCode) {
ruleID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new instance rule: %s", err), "error creating rule ID")
}
rule := &gtsmodel.Rule{
ID: ruleID,
Text: form.Text,
}
if err = p.state.DB.PutRule(ctx, rule); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return p.tc.InstanceRuleToAdminAPIRule(rule), nil
}
// RuleUpdate updates text for an existing rule.
func (p *Processor) RuleUpdate(ctx context.Context, id string, form *apimodel.InstanceRuleCreateRequest) (*apimodel.AdminInstanceRule, gtserror.WithCode) {
rule, err := p.state.DB.GetRuleByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("RuleUpdate: no rule with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("RuleUpdate: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
rule.Text = form.Text
updatedRule, err := p.state.DB.UpdateRule(ctx, rule)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return p.tc.InstanceRuleToAdminAPIRule(updatedRule), nil
}
// RuleDelete deletes an existing rule.
func (p *Processor) RuleDelete(ctx context.Context, id string) (*apimodel.AdminInstanceRule, gtserror.WithCode) {
rule, err := p.state.DB.GetRuleByID(ctx, id)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("RuleUpdate: no rule with id %s found in the db", id)
return nil, gtserror.NewErrorNotFound(err)
}
err := fmt.Errorf("RuleUpdate: db error: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
rule.Deleted = util.Ptr(true)
deletedRule, err := p.state.DB.UpdateRule(ctx, rule)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return p.tc.InstanceRuleToAdminAPIRule(deletedRule), nil
}

View file

@ -136,6 +136,15 @@ func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool,
return domains, nil
}
func (p *Processor) InstanceGetRules(ctx context.Context) ([]apimodel.InstanceRule, gtserror.WithCode) {
i, err := p.getThisInstance(ctx)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err))
}
return p.tc.InstanceRulesToAPIRules(i.Rules), nil
}
func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) {
// fetch the instance entry from the db for processing
host := config.GetHost()

View file

@ -64,6 +64,13 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
}
}
// fetch rules by IDs given in the report form (noop if no rules given)
rules, err := p.state.DB.GetRulesByIDs(ctx, form.RuleIDs)
if err != nil {
err = fmt.Errorf("db error fetching report target rules: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
reportID := id.NewULID()
report := &gtsmodel.Report{
ID: reportID,
@ -75,6 +82,8 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
Comment: form.Comment,
StatusIDs: form.StatusIDs,
Statuses: statuses,
RuleIDs: form.RuleIDs,
Rules: rules,
Forwarded: &form.Forward,
}

View file

@ -83,6 +83,10 @@ type TypeConverter interface {
InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error)
// InstanceToAPIV2Instance converts a gts instance into its api equivalent for serving at /api/v2/instance
InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV2, error)
// InstanceRulesToAPIRules converts all local instance rules into their api equivalent for serving at /api/v1/instance/rules
InstanceRulesToAPIRules(r []gtsmodel.Rule) []apimodel.InstanceRule
// InstanceRuleToAdminAPIRule converts a local instance rule into its api equivalent for serving at /api/v1/admin/instance/rules/:id
InstanceRuleToAdminAPIRule(r *gtsmodel.Rule) *apimodel.AdminInstanceRule
// RelationshipToAPIRelationship converts a gts relationship into its api equivalent for serving in various places
RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error)
// NotificationToAPINotification converts a gts notification into a api notification

View file

@ -738,6 +738,32 @@ func (c *converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apim
return ""
}
func (c *converter) InstanceRuleToAPIRule(r gtsmodel.Rule) apimodel.InstanceRule {
return apimodel.InstanceRule{
ID: r.ID,
Text: r.Text,
}
}
func (c *converter) InstanceRulesToAPIRules(r []gtsmodel.Rule) []apimodel.InstanceRule {
rules := make([]apimodel.InstanceRule, len(r))
for i, v := range r {
rules[i] = c.InstanceRuleToAPIRule(v)
}
return rules
}
func (c *converter) InstanceRuleToAdminAPIRule(r *gtsmodel.Rule) *apimodel.AdminInstanceRule {
return &apimodel.AdminInstanceRule{
ID: r.ID,
CreatedAt: util.FormatISO8601(r.CreatedAt),
UpdatedAt: util.FormatISO8601(r.UpdatedAt),
Text: r.Text,
}
}
func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error) {
instance := &apimodel.InstanceV1{
URI: i.URI,
@ -752,6 +778,7 @@ func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
ApprovalRequired: config.GetAccountsApprovalRequired(),
InvitesEnabled: false, // todo: not supported yet
MaxTootChars: uint(config.GetStatusesMaxChars()),
Rules: c.InstanceRulesToAPIRules(i.Rules),
}
if config.GetInstanceInjectMastodonVersion() {
@ -854,7 +881,7 @@ func (c *converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
Description: i.Description,
Usage: apimodel.InstanceV2Usage{}, // todo: not implemented
Languages: []string{}, // todo: not implemented
Rules: []interface{}{}, // todo: not implemented
Rules: c.InstanceRulesToAPIRules(i.Rules),
}
if config.GetInstanceInjectMastodonVersion() {
@ -1051,7 +1078,7 @@ func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (
Comment: r.Comment,
Forwarded: *r.Forwarded,
StatusIDs: r.StatusIDs,
RuleIDs: []int{}, // todo: not supported yet
RuleIDs: r.RuleIDs,
}
if !r.ActionTakenAt.IsZero() {
@ -1144,6 +1171,20 @@ func (c *converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
statuses = append(statuses, status)
}
rules := make([]*apimodel.InstanceRule, 0, len(r.RuleIDs))
if len(r.RuleIDs) != 0 && len(r.Rules) == 0 {
r.Rules, err = c.db.GetRulesByIDs(ctx, r.RuleIDs)
if err != nil {
return nil, fmt.Errorf("ReportToAdminAPIReport: error getting rules from the db: %w", err)
}
}
for _, v := range r.Rules {
rules = append(rules, &apimodel.InstanceRule{
ID: v.ID,
Text: v.Text,
})
}
if ac := r.ActionTaken; ac != "" {
actionTakenComment = &ac
}
@ -1163,7 +1204,7 @@ func (c *converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
ActionTakenByAccount: actionTakenByAccount,
ActionTakenComment: actionTakenComment,
Statuses: statuses,
Rules: []interface{}{}, // not implemented
Rules: rules,
}, nil
}

View file

@ -603,6 +603,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
b, err := json.MarshalIndent(instance, "", " ")
suite.NoError(err)
// FIXME: "rules" is empty from the database, because it's not fetched through db.GetInstance
suite.Equal(`{
"uri": "http://localhost:8080",
"account_domain": "localhost:8080",
@ -689,7 +690,8 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
"name": "admin"
}
},
"max_toot_chars": 5000
"max_toot_chars": 5000,
"rules": []
}`, string(b))
}
@ -887,7 +889,10 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend1() {
"status_ids": [
"01FVW7JHQFSFK166WWKR8CBA6M"
],
"rule_ids": [],
"rule_ids": [
"01GP3AWY4CRDVRNZKW0TEAMB51",
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
],
"target_account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",
@ -1177,7 +1182,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},
"statuses": [],
"rule_ids": [],
"rules": [],
"action_taken_comment": "user was warned not to be a turtle anymore"
}`, string(b))
}
@ -1380,7 +1385,16 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
"poll": null
}
],
"rule_ids": [],
"rules": [
{
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
"text": "Be gay"
},
{
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
"text": "Do crime"
}
],
"action_taken_comment": null
}`, string(b))
}
@ -1603,7 +1617,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},
"statuses": [],
"rule_ids": [],
"rules": [],
"action_taken_comment": "user was warned not to be a turtle anymore"
}`, string(b))
}