diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
index da25d29c5..a717139cf 100644
--- a/docs/api/swagger.yaml
+++ b/docs/api/swagger.yaml
@@ -566,11 +566,12 @@ definitions:
example: 01FBVD42CQ3ZEEVMW180SBX03B
type: string
x-go-name: ID
- rule_ids:
+ rules:
description: |-
- Array of rule IDs that were submitted along with this report.
- NOT IMPLEMENTED, will always be empty array.
- items: {}
+ Array of rules that were broken according to this report.
+ Will be empty if no rule IDs were submitted with the report.
+ items:
+ $ref: '#/definitions/instanceRule'
type: array
x-go-name: Rules
statuses:
@@ -1274,6 +1275,36 @@ definitions:
type: object
x-go-name: InstanceConfigurationStatuses
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ instanceRule:
+ properties:
+ id:
+ type: string
+ x-go-name: ID
+ text:
+ type: string
+ x-go-name: Text
+ title: InstanceRule represents a single instance rule.
+ type: object
+ x-go-name: InstanceRule
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ instanceRuleCreateRequest:
+ properties:
+ Text:
+ type: string
+ title: InstanceRuleCreateRequest represents a request to create a new instance rule, made through the admin API.
+ type: object
+ x-go-name: InstanceRuleCreateRequest
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ instanceRuleUpdateRequest:
+ properties:
+ ID:
+ type: string
+ Text:
+ type: string
+ title: InstanceRuleUpdateRequest represents a request to update the text of an instance rule, made through the admin API.
+ type: object
+ x-go-name: InstanceRuleUpdateRequest
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
instanceV1:
properties:
account_domain:
@@ -1330,6 +1361,12 @@ definitions:
description: New account registrations are enabled on this instance.
type: boolean
x-go-name: Registrations
+ rules:
+ description: An itemized list of rules for this instance.
+ items:
+ $ref: '#/definitions/instanceRule'
+ type: array
+ x-go-name: Rules
short_description:
description: |-
A shorter description of the instance.
@@ -1453,10 +1490,9 @@ definitions:
registrations:
$ref: '#/definitions/instanceV2Registrations'
rules:
- description: |-
- An itemized list of rules for this website.
- Currently not implemented (will always be empty array).
- items: {}
+ description: An itemized list of rules for this instance.
+ items:
+ $ref: '#/definitions/instanceRule'
type: array
x-go-name: Rules
source_url:
@@ -1755,6 +1791,72 @@ definitions:
type: object
x-go-name: MediaMeta
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ multiStatus:
+ description: |-
+ This model should be transmitted along with http code
+ 207 MULTI-STATUS to indicate a mixture of responses.
+ See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/207
+ properties:
+ data:
+ items:
+ $ref: '#/definitions/multiStatusEntry'
+ type: array
+ x-go-name: Data
+ metadata:
+ $ref: '#/definitions/multiStatusMetadata'
+ title: MultiStatus models a multistatus HTTP response body.
+ type: object
+ x-go-name: MultiStatus
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ multiStatusEntry:
+ description: |-
+ It can model either a success or a failure. The type
+ and value of `Resource` is left to the discretion of
+ the caller, but at minimum it should be expected to be
+ JSON-serializable.
+ properties:
+ message:
+ description: Message/error message for this entry.
+ type: string
+ x-go-name: Message
+ resource:
+ description: |-
+ The resource/result for this entry.
+ Value may be any type, check the docs
+ per endpoint to see which to expect.
+ x-go-name: Resource
+ status:
+ description: HTTP status code of this entry.
+ format: int64
+ type: integer
+ x-go-name: Status
+ title: MultiStatusEntry models one entry in multistatus data.
+ type: object
+ x-go-name: MultiStatusEntry
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ multiStatusMetadata:
+ description: |-
+ MultiStatusMetadata models an at-a-glance summary of
+ the data contained in the MultiStatus.
+ properties:
+ failure:
+ description: Count of unsuccessful results (!2xx).
+ format: int64
+ type: integer
+ x-go-name: Failure
+ success:
+ description: Count of successful results (2xx).
+ format: int64
+ type: integer
+ x-go-name: Success
+ total:
+ description: Success count + failure count.
+ format: int64
+ type: integer
+ x-go-name: Total
+ type: object
+ x-go-name: MultiStatusMetadata
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
nodeinfo:
description: 'See: https://nodeinfo.diaspora.software/schema.html'
properties:
@@ -1971,11 +2073,10 @@ definitions:
Array of rule IDs that were submitted along with this report.
Will be empty if no rule IDs were submitted.
example:
- - 1
- - 2
+ - 01GPBN5YDY6JKBWE44H7YQBDCQ
+ - 01GPBN65PDWSBPWVDD0SQCFFY3
items:
- format: int64
- type: integer
+ type: string
type: array
x-go-name: RuleIDs
status_ids:
@@ -4036,6 +4137,118 @@ paths:
summary: Send a generic test email to a specified email address.
tags:
- admin
+ /api/v1/admin/instance/rules:
+ post:
+ consumes:
+ - multipart/form-data
+ operationId: ruleCreate
+ parameters:
+ - description: Text body for the instance rule, plaintext.
+ in: formData
+ name: text
+ required: true
+ type: string
+ produces:
+ - application/json
+ 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
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: Create a new instance rule.
+ tags:
+ - admin
+ /api/v1/admin/instance/rules{id}:
+ delete:
+ consumes:
+ - multipart/form-data
+ operationId: ruleDelete
+ parameters:
+ - description: The id of the rule to delete.
+ in: formData
+ name: id
+ required: true
+ type: path
+ produces:
+ - application/json
+ 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
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: Delete an existing instance rule.
+ tags:
+ - admin
+ patch:
+ consumes:
+ - multipart/form-data
+ operationId: ruleUpdate
+ parameters:
+ - description: The id of the rule to update.
+ in: formData
+ name: id
+ required: true
+ type: path
+ - description: Text body for the updated instance rule, plaintext.
+ in: formData
+ name: text
+ required: true
+ type: string
+ produces:
+ - application/json
+ 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
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: Update an existing instance rule.
+ tags:
+ - admin
/api/v1/admin/media_cleanup:
post:
consumes:
@@ -4251,6 +4464,67 @@ paths:
summary: Mark a report as resolved.
tags:
- admin
+ /api/v1/admin/rules:
+ get:
+ description: The rules will be returned in order (sorted by Order ascending).
+ operationId: rules
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: An array with all the rules for the local instance.
+ schema:
+ items:
+ $ref: '#/definitions/instanceRule'
+ type: array
+ "400":
+ description: bad request
+ "401":
+ description: unauthorized
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: View instance rules, with IDs.
+ tags:
+ - admin
+ /api/v1/admin/rules/{id}:
+ get:
+ operationId: adminRuleGet
+ parameters:
+ - description: The id of the rule.
+ in: path
+ name: id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ 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
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: View instance rule with the given id.
+ tags:
+ - admin
/api/v1/apps:
post:
consumes:
@@ -4750,6 +5024,30 @@ paths:
description: internal server error
tags:
- instance
+ /api/v1/instance/rules:
+ get:
+ description: The rules will be returned in order (sorted by Order ascending).
+ operationId: rules
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: An array with all the rules for the local instance.
+ schema:
+ items:
+ $ref: '#/definitions/instanceRule'
+ type: array
+ "400":
+ description: bad request
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
+ summary: View instance rules (public).
+ tags:
+ - instance
/api/v1/lists:
get:
operationId: lists
@@ -5505,17 +5803,13 @@ paths:
name: category
type: string
x-go-name: Category
- - description: |-
- IDs of rules on this instance which have been broken according to the reporter.
- This is currently not supported, provided only for API compatibility.
+ - description: IDs of rules on this instance which have been broken according to the reporter.
example:
- - 1
- - 2
- - 3
+ - 01GPBN5YDY6JKBWE44H7YQBDCQ
+ - 01GPBN65PDWSBPWVDD0SQCFFY3
in: formData
items:
- format: int64
- type: integer
+ type: string
name: rule_ids
type: array
x-go-name: RuleIDs
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go
index a6c825b2b..ce6604c29 100644
--- a/internal/api/client/admin/admin.go
+++ b/internal/api/client/admin/admin.go
@@ -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)
}
diff --git a/internal/api/client/admin/reportsget_test.go b/internal/api/client/admin/reportsget_test.go
index 943e9711a..4c714a9e0 100644
--- a/internal/api/client/admin/reportsget_test.go
+++ b/internal/api/client/admin/reportsget_test.go
@@ -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))
diff --git a/internal/api/client/admin/rulecreate.go b/internal/api/client/admin/rulecreate.go
new file mode 100644
index 000000000..7792233f6
--- /dev/null
+++ b/internal/api/client/admin/rulecreate.go
@@ -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 .
+
+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
+}
diff --git a/internal/api/client/admin/ruledelete.go b/internal/api/client/admin/ruledelete.go
new file mode 100644
index 000000000..7281ed62e
--- /dev/null
+++ b/internal/api/client/admin/ruledelete.go
@@ -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 .
+
+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)
+}
diff --git a/internal/api/client/admin/ruleget.go b/internal/api/client/admin/ruleget.go
new file mode 100644
index 000000000..444820a3f
--- /dev/null
+++ b/internal/api/client/admin/ruleget.go
@@ -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 .
+
+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)
+}
diff --git a/internal/api/client/admin/rulesget.go b/internal/api/client/admin/rulesget.go
new file mode 100644
index 000000000..56f83866f
--- /dev/null
+++ b/internal/api/client/admin/rulesget.go
@@ -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 .
+
+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)
+}
diff --git a/internal/api/client/admin/ruleupdate.go b/internal/api/client/admin/ruleupdate.go
new file mode 100644
index 000000000..82ed41190
--- /dev/null
+++ b/internal/api/client/admin/ruleupdate.go
@@ -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 .
+
+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)
+}
diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go
index 8c58b62aa..82f6a4714 100644
--- a/internal/api/client/instance/instance.go
+++ b/internal/api/client/instance/instance.go
@@ -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)
}
diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go
index 11382f83a..a402f8347 100644
--- a/internal/api/client/instance/instancepatch_test.go
+++ b/internal/api/client/instance/instancepatch_test.go
@@ -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())
}
diff --git a/internal/api/client/instance/instancerulesget.go b/internal/api/client/instance/instancerulesget.go
new file mode 100644
index 000000000..5cc99ba41
--- /dev/null
+++ b/internal/api/client/instance/instancerulesget.go
@@ -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 .
+
+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)
+}
diff --git a/internal/api/client/reports/reportcreate_test.go b/internal/api/client/reports/reportcreate_test.go
index e17695cb9..35dc3d015 100644
--- a/internal/api/client/reports/reportcreate_test.go
+++ b/internal/api/client/reports/reportcreate_test.go
@@ -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
diff --git a/internal/api/client/reports/reportget_test.go b/internal/api/client/reports/reportget_test.go
index e29836b6a..1bdb7557c 100644
--- a/internal/api/client/reports/reportget_test.go
+++ b/internal/api/client/reports/reportget_test.go
@@ -108,7 +108,10 @@ func (suite *ReportGetTestSuite) TestGetReport1() {
"status_ids": [
"01FVW7JHQFSFK166WWKR8CBA6M"
],
- "rule_ids": [],
+ "rule_ids": [
+ "01GP3AWY4CRDVRNZKW0TEAMB51",
+ "01GP3DFY9XQ1TJMZT5BGAZPXX3"
+ ],
"target_account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",
diff --git a/internal/api/client/reports/reportsget_test.go b/internal/api/client/reports/reportsget_test.go
index d220dc94d..e58a622db 100644
--- a/internal/api/client/reports/reportsget_test.go
+++ b/internal/api/client/reports/reportsget_test.go
@@ -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",
diff --git a/internal/api/model/admin.go b/internal/api/model/admin.go
index cc449ab82..860cb8926 100644
--- a/internal/api/model/admin.go
+++ b/internal/api/model/admin.go
@@ -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
+}
diff --git a/internal/api/model/instancev1.go b/internal/api/model/instancev1.go
index 19682c1f1..3b3d215b0 100644
--- a/internal/api/model/instancev1.go
+++ b/internal/api/model/instancev1.go
@@ -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.
diff --git a/internal/api/model/instancev2.go b/internal/api/model/instancev2.go
index 25d9c790d..3099b36c4 100644
--- a/internal/api/model/instancev2.go
+++ b/internal/api/model/instancev2.go
@@ -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.
diff --git a/internal/api/model/report.go b/internal/api/model/report.go
index eb68e7911..b9b8c77d2 100644
--- a/internal/api/model/report.go
+++ b/internal/api/model/report.go
@@ -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"`
}
diff --git a/internal/api/model/rule.go b/internal/api/model/rule.go
new file mode 100644
index 000000000..f4caf7dd0
--- /dev/null
+++ b/internal/api/model/rule.go
@@ -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 .
+
+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"`
+}
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index ad9053e6e..e92234f81 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -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,
diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go
index 0cdbb5cce..f3640cf59 100644
--- a/internal/db/bundb/bundb_test.go
+++ b/internal/db/bundb/bundb_test.go
@@ -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() {
diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go
index 7f0e92634..6fec3f2fe 100644
--- a/internal/db/bundb/instance.go
+++ b/internal/db/bundb/instance.go
@@ -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 {
diff --git a/internal/db/bundb/migrations/20230815164500_rules_model.go b/internal/db/bundb/migrations/20230815164500_rules_model.go
new file mode 100644
index 000000000..9b202ede9
--- /dev/null
+++ b/internal/db/bundb/migrations/20230815164500_rules_model.go
@@ -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 .
+
+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(>smodel.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)
+ }
+}
diff --git a/internal/db/bundb/migrations/20230817174700_add_report_rule_ids.go b/internal/db/bundb/migrations/20230817174700_add_report_rule_ids.go
new file mode 100644
index 000000000..a66739e4c
--- /dev/null
+++ b/internal/db/bundb/migrations/20230817174700_add_report_rule_ids.go
@@ -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 .
+
+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)
+ }
+}
diff --git a/internal/db/bundb/report.go b/internal/db/bundb/report.go
index 7c1dd16e7..9e4ba5b29 100644
--- a/internal/db/bundb/report.go
+++ b/internal/db/bundb/report.go
@@ -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.
diff --git a/internal/db/bundb/rule.go b/internal/db/bundb/rule.go
new file mode 100644
index 000000000..79825923b
--- /dev/null
+++ b/internal/db/bundb/rule.go
@@ -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 .
+
+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
+}
diff --git a/internal/db/bundb/rule_test.go b/internal/db/bundb/rule_test.go
new file mode 100644
index 000000000..822f92fca
--- /dev/null
+++ b/internal/db/bundb/rule_test.go
@@ -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 .
+
+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 := >smodel.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 := >smodel.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))
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index 567551c73..056d03e23 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -38,6 +38,7 @@ type DB interface {
Notification
Relationship
Report
+ Rule
Search
Session
Status
diff --git a/internal/db/rule.go b/internal/db/rule.go
new file mode 100644
index 000000000..651b8bced
--- /dev/null
+++ b/internal/db/rule.go
@@ -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 .
+
+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)
+}
diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go
index 388f0f4ed..6d572f519 100644
--- a/internal/gtsmodel/instance.go
+++ b/internal/gtsmodel/instance.go
@@ -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
}
diff --git a/internal/gtsmodel/report.go b/internal/gtsmodel/report.go
index e5b942563..b332ec348 100644
--- a/internal/gtsmodel/report.go
+++ b/internal/gtsmodel/report.go
@@ -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
diff --git a/internal/gtsmodel/rule.go b/internal/gtsmodel/rule.go
new file mode 100644
index 000000000..76fa6f7bd
--- /dev/null
+++ b/internal/gtsmodel/rule.go
@@ -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 .
+
+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
+}
diff --git a/internal/processing/admin/rule.go b/internal/processing/admin/rule.go
new file mode 100644
index 000000000..40a2bdcf3
--- /dev/null
+++ b/internal/processing/admin/rule.go
@@ -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 .
+
+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 := >smodel.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
+}
diff --git a/internal/processing/instance.go b/internal/processing/instance.go
index edcfe5418..2faef7527 100644
--- a/internal/processing/instance.go
+++ b/internal/processing/instance.go
@@ -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()
diff --git a/internal/processing/report/create.go b/internal/processing/report/create.go
index a6cce8e80..48f9c1ee4 100644
--- a/internal/processing/report/create.go
+++ b/internal/processing/report/create.go
@@ -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 := >smodel.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,
}
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 73992fc0e..774b68157 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -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
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index ab04f6ccc..050997bda 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -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
}
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index d99a31e25..9f72c6d2e 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -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))
}
diff --git a/testrig/db.go b/testrig/db.go
index 4d8dfefa5..57e94a4bf 100644
--- a/testrig/db.go
+++ b/testrig/db.go
@@ -61,6 +61,7 @@ var testModels = []interface{}{
>smodel.EmojiCategory{},
>smodel.Tombstone{},
>smodel.Report{},
+ >smodel.Rule{},
>smodel.AccountNote{},
}
@@ -160,6 +161,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
}
}
+ for _, v := range NewTestRules() {
+ if err := db.Put(ctx, v); err != nil {
+ log.Panic(nil, err)
+ }
+ }
+
for _, v := range NewTestDomainBlocks() {
if err := db.Put(ctx, v); err != nil {
log.Panic(nil, err)
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index f5f63f6fa..4f0768b45 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -2021,6 +2021,7 @@ func NewTestReports() map[string]*gtsmodel.Report {
Comment: "dark souls sucks, please yeet this nerd",
StatusIDs: []string{"01FVW7JHQFSFK166WWKR8CBA6M"},
Forwarded: util.Ptr(true),
+ RuleIDs: []string{"01GP3AWY4CRDVRNZKW0TEAMB51", "01GP3DFY9XQ1TJMZT5BGAZPXX3"},
},
"remote_account_1_report_local_account_2": {
ID: "01GP3DFY9XQ1TJMZT5BGAZPXX7",
@@ -2031,6 +2032,7 @@ func NewTestReports() map[string]*gtsmodel.Report {
TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
Comment: "this is a turtle, not a person, therefore should not be a poster",
StatusIDs: []string{},
+ RuleIDs: []string{},
Forwarded: util.Ptr(true),
ActionTaken: "user was warned not to be a turtle anymore",
ActionTakenAt: TimeMustParse("2022-05-15T17:01:56+02:00"),
@@ -2039,6 +2041,35 @@ func NewTestReports() map[string]*gtsmodel.Report {
}
}
+func NewTestRules() map[string]*gtsmodel.Rule {
+ return map[string]*gtsmodel.Rule{
+ "rule1": {
+ ID: "01GP3AWY4CRDVRNZKW0TEAMB51",
+ CreatedAt: TimeMustParse("2022-05-14T12:20:03+02:00"),
+ UpdatedAt: TimeMustParse("2022-05-14T12:20:03+02:00"),
+ Text: "Be gay",
+ Deleted: util.Ptr(false),
+ Order: util.Ptr(uint(0)),
+ },
+ "deleted_rule": {
+ ID: "01GP3DFY9XQ1TJMZT5BGAZPXX2",
+ CreatedAt: TimeMustParse("2022-05-15T16:20:12+02:00"),
+ UpdatedAt: TimeMustParse("2022-05-15T16:20:12+02:00"),
+ Text: "Deleted",
+ Deleted: util.Ptr(true),
+ Order: util.Ptr(uint(1)),
+ },
+ "rule2": {
+ ID: "01GP3DFY9XQ1TJMZT5BGAZPXX3",
+ CreatedAt: TimeMustParse("2022-05-15T16:20:12+02:00"),
+ UpdatedAt: TimeMustParse("2022-05-15T16:20:12+02:00"),
+ Text: "Do crime",
+ Deleted: util.Ptr(false),
+ Order: util.Ptr(uint(2)),
+ },
+ }
+}
+
// ActivityWithSignature wraps a pub.Activity along with its signature headers, for testing.
type ActivityWithSignature struct {
Activity pub.Activity
diff --git a/web/source/css/base.css b/web/source/css/base.css
index 87d2fcca7..5cd2cd047 100644
--- a/web/source/css/base.css
+++ b/web/source/css/base.css
@@ -542,6 +542,57 @@ label {
}
}
+.instance-rules {
+ list-style-position: inside;
+ margin: 0;
+ padding: 0;
+
+ a.rule {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ color: $fg;
+ text-decoration: none;
+ background: $toot-bg;
+ padding: 1rem;
+ margin: 0.5rem 0;
+ border-radius: $br;
+ line-height: 2rem;
+ position: relative;
+
+ &:hover {
+ color: $fg-accent;
+
+ .edit-icon {
+ display: inline;
+ }
+ }
+
+ .edit-icon {
+ display: none;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ }
+
+ li {
+ font-size: 1.75rem;
+ padding: 0;
+ margin: 0;
+
+ h2 {
+ margin: 0;
+ margin-top: 0 !important;
+ display: inline-block;
+ font-size: 1.5rem;
+ }
+ }
+
+ span {
+ color: $fg-reduced;
+ }
+ }
+}
+
@media screen and (max-width: 30rem) {
.domain-blocklist .entry {
grid-template-columns: 1fr;
diff --git a/web/source/settings/admin/federation/detail.js b/web/source/settings/admin/federation/detail.js
index 344b9f9b6..a3bbfcac1 100644
--- a/web/source/settings/admin/federation/detail.js
+++ b/web/source/settings/admin/federation/detail.js
@@ -141,22 +141,29 @@ function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) {
{...disabledForm}
/>
-
-
- {
- isExistingBlock &&
+
removeBlock(block.id)}
- label="Remove"
- result={removeResult}
- className="button danger"
+ label="Suspend"
+ result={addResult}
+ showError={false}
+ {...disabledForm}
/>
- }
+
+ {
+ isExistingBlock &&
+ removeBlock(block.id)}
+ label="Remove"
+ result={removeResult}
+ className="button danger"
+ showError={false}
+ />
+ }
+
+
+ {addResult.error && }
+ {removeResult.error && }
);
diff --git a/web/source/settings/admin/settings.js b/web/source/settings/admin/settings/index.jsx
similarity index 91%
rename from web/source/settings/admin/settings.js
rename to web/source/settings/admin/settings/index.jsx
index ec986a6c4..dab476433 100644
--- a/web/source/settings/admin/settings.js
+++ b/web/source/settings/admin/settings/index.jsx
@@ -21,23 +21,23 @@
const React = require("react");
-const query = require("../lib/query");
+const query = require("../../lib/query");
const {
useTextInput,
useFileInput
-} = require("../lib/form");
+} = require("../../lib/form");
-const useFormSubmit = require("../lib/form/submit");
+const useFormSubmit = require("../../lib/form/submit");
const {
TextInput,
TextArea,
FileInput
-} = require("../components/form/inputs");
+} = require("../../components/form/inputs");
-const FormWithData = require("../lib/form/form-with-data");
-const MutationButton = require("../components/form/mutation-button");
+const FormWithData = require("../../lib/form/form-with-data");
+const MutationButton = require("../../components/form/mutation-button");
module.exports = function AdminSettings() {
return (
diff --git a/web/source/settings/admin/settings/rules.jsx b/web/source/settings/admin/settings/rules.jsx
new file mode 100644
index 000000000..330bc07fd
--- /dev/null
+++ b/web/source/settings/admin/settings/rules.jsx
@@ -0,0 +1,169 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+const { Switch, Route, Link, Redirect, useRoute } = require("wouter");
+
+const query = require("../../lib/query");
+const FormWithData = require("../../lib/form/form-with-data");
+const { useBaseUrl } = require("../../lib/navigation/util");
+
+const { useValue, useTextInput } = require("../../lib/form");
+const useFormSubmit = require("../../lib/form/submit");
+
+const { TextArea } = require("../../components/form/inputs");
+const MutationButton = require("../../components/form/mutation-button");
+
+module.exports = function InstanceRulesData({ baseUrl }) {
+ return (
+
+ );
+};
+
+function InstanceRules({ baseUrl, data: rules }) {
+ return (
+
+
+
+
+
+
+
Instance Rules
+
+
+ The rules for your instance are listed on the about page, and can be selected when submitting reports.
+
+
+
+
+
+
+ );
+}
+
+function InstanceRuleList({ rules }) {
+ const newRule = useTextInput("text", {});
+
+ const [submitForm, result] = useFormSubmit({ newRule }, query.useAddInstanceRuleMutation(), {
+ onFinish: () => newRule.reset()
+ });
+
+ return (
+ <>
+
+ >
+ );
+}
+
+function InstanceRule({ rule }) {
+ const baseUrl = useBaseUrl();
+
+ return (
+
+
+
+ {rule.text}
+
+ {new Date(rule.created_at).toLocaleString()}
+
+
+ );
+}
+
+function InstanceRuleDetail({ rules }) {
+ const baseUrl = useBaseUrl();
+ let [_match, params] = useRoute(`${baseUrl}/:ruleId`);
+
+ if (params?.ruleId == undefined || rules[params.ruleId] == undefined) {
+ return ;
+ } else {
+ return (
+ <>
+ < go back
+
+ >
+ );
+ }
+}
+
+function InstanceRuleForm({ rule }) {
+ const baseUrl = useBaseUrl();
+ const form = {
+ id: useValue("id", rule.id),
+ rule: useTextInput("text", { defaultValue: rule.text })
+ };
+
+ const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceRuleMutation());
+
+ const [deleteRule, deleteResult] = query.useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id });
+
+ if (result.isSuccess || deleteResult.isSuccess) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ deleteRule(rule.id)}
+ label="Delete"
+ className="button danger"
+ showError={false}
+ result={deleteResult}
+ />
+
+
+ {result.error && }
+ {deleteResult.error && }
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/source/settings/index.js b/web/source/settings/index.js
index 8eb11e0aa..398bca0f6 100644
--- a/web/source/settings/index.js
+++ b/web/source/settings/index.js
@@ -60,7 +60,10 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
Item("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
]),
- Item("Settings", { icon: "fa-sliders" }, require("./admin/settings"))
+ Menu("Settings", { icon: "fa-sliders" }, [
+ Item("Settings", { icon: "fa-sliders", url: "" }, require("./admin/settings")),
+ Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, require("./admin/settings/rules"))
+ ])
])
]);
diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js
index dd4a61b51..515d8edcf 100644
--- a/web/source/settings/lib/query/admin/index.js
+++ b/web/source/settings/lib/query/admin/index.js
@@ -22,7 +22,8 @@
const {
replaceCacheOnMutation,
removeFromCacheOnMutation,
- domainListToObject
+ domainListToObject,
+ idListToObject
} = require("../lib");
const base = require("../base");
@@ -104,6 +105,51 @@ const endpoints = (build) => ({
return res.accounts ?? [];
}
}),
+ instanceRules: build.query({
+ query: () => ({
+ url: `/api/v1/admin/instance/rules`
+ }),
+ transformResponse: idListToObject
+ }),
+ addInstanceRule: build.mutation({
+ query: (formData) => ({
+ method: "POST",
+ url: `/api/v1/admin/instance/rules`,
+ asForm: true,
+ body: formData,
+ discardEmpty: true
+ }),
+ transformResponse: (data) => {
+ return {
+ [data.id]: data
+ };
+ },
+ ...replaceCacheOnMutation("instanceRules")
+ }),
+ updateInstanceRule: build.mutation({
+ query: ({ id, ...edit }) => ({
+ method: "PATCH",
+ url: `/api/v1/admin/instance/rules/${id}`,
+ asForm: true,
+ body: edit,
+ discardEmpty: true
+ }),
+ transformResponse: (data) => {
+ return {
+ [data.id]: data
+ };
+ },
+ ...replaceCacheOnMutation("instanceRules")
+ }),
+ deleteInstanceRule: build.mutation({
+ query: (id) => ({
+ method: "DELETE",
+ url: `/api/v1/admin/instance/rules/${id}`
+ }),
+ ...removeFromCacheOnMutation("instanceRules", {
+ findKey: (_draft, rule) => rule.id
+ })
+ }),
...require("./import-export")(build),
...require("./custom-emoji")(build),
...require("./reports")(build)
diff --git a/web/source/settings/lib/query/base.js b/web/source/settings/lib/query/base.js
index 653fc449b..ba02d4e07 100644
--- a/web/source/settings/lib/query/base.js
+++ b/web/source/settings/lib/query/base.js
@@ -59,7 +59,7 @@ function instanceBasedQuery(args, api, extraOptions) {
module.exports = createApi({
reducerPath: "api",
baseQuery: instanceBasedQuery,
- tagTypes: ["Auth", "Emoji", "Reports", "Account"],
+ tagTypes: ["Auth", "Emoji", "Reports", "Account", "InstanceRules"],
endpoints: (build) => ({
instance: build.query({
query: () => ({
diff --git a/web/source/settings/lib/query/lib.js b/web/source/settings/lib/query/lib.js
index 78a9291b7..56ce05478 100644
--- a/web/source/settings/lib/query/lib.js
+++ b/web/source/settings/lib/query/lib.js
@@ -37,6 +37,13 @@ module.exports = {
(_) => Object.fromEntries(_)
]);
},
+ idListToObject: (data) => {
+ // Turn flat Array into Object keyed by entry id field
+ return syncpipe(data, [
+ (_) => _.map((entry) => [entry.id, entry]),
+ (_) => Object.fromEntries(_)
+ ]);
+ },
replaceCacheOnMutation: makeCacheMutation((draft, newData) => {
Object.assign(draft, newData);
}),
diff --git a/web/template/about.tmpl b/web/template/about.tmpl
index 6c22b98ee..34fbe2570 100644
--- a/web/template/about.tmpl
+++ b/web/template/about.tmpl
@@ -26,7 +26,7 @@
-
Features
+
Rules
+
+ {{range .instance.Rules}}
+ {{.Text}}
+ {{end}}
+
+
+
+
+
Features
Registration is
@@ -68,8 +77,9 @@
+
-
Moderated servers
+
Moderated servers
ActivityPub instances exchange (federate) data with other servers, including accounts and toots.
This can be prevented for specific domains by suspending them. None of their content is stored,
@@ -83,12 +93,12 @@
-
Instance Statistics
-
- Users: {{.instance.Stats.user_count}}
- Posts: {{.instance.Stats.status_count}}
- Federates with: {{.instance.Stats.domain_count}} instances
-
+
Instance Statistics
+
+ Users: {{.instance.Stats.user_count}}
+ Posts: {{.instance.Stats.status_count}}
+ Federates with: {{.instance.Stats.domain_count}} instances
+