mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
[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:
parent
d5d6ad406f
commit
92de8fb396
49 changed files with 2189 additions and 107 deletions
|
@ -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
|
||||
|
|
|
@ -25,22 +25,24 @@
|
|||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
120
internal/api/client/admin/rulecreate.go
Normal file
120
internal/api/client/admin/rulecreate.go
Normal 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
|
||||
}
|
107
internal/api/client/admin/ruledelete.go
Normal file
107
internal/api/client/admin/ruledelete.go
Normal 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)
|
||||
}
|
102
internal/api/client/admin/ruleget.go
Normal file
102
internal/api/client/admin/ruleget.go
Normal 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)
|
||||
}
|
91
internal/api/client/admin/rulesget.go
Normal file
91
internal/api/client/admin/rulesget.go
Normal 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)
|
||||
}
|
127
internal/api/client/admin/ruleupdate.go
Normal file
127
internal/api/client/admin/ruleupdate.go
Normal 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)
|
||||
}
|
|
@ -28,6 +28,7 @@
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
71
internal/api/client/instance/instancerulesget.go
Normal file
71
internal/api/client/instance/instancerulesget.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -108,7 +108,10 @@ func (suite *ReportGetTestSuite) TestGetReport1() {
|
|||
"status_ids": [
|
||||
"01FVW7JHQFSFK166WWKR8CBA6M"
|
||||
],
|
||||
"rule_ids": [],
|
||||
"rule_ids": [
|
||||
"01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
|
||||
],
|
||||
"target_account": {
|
||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||
"username": "foss_satan",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
41
internal/api/model/rule.go
Normal file
41
internal/api/model/rule.go
Normal 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"`
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
47
internal/db/bundb/migrations/20230815164500_rules_model.go
Normal file
47
internal/db/bundb/migrations/20230815164500_rules_model.go
Normal 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(>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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
149
internal/db/bundb/rule.go
Normal 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
|
||||
}
|
122
internal/db/bundb/rule_test.go
Normal file
122
internal/db/bundb/rule_test.go
Normal 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 := >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))
|
||||
}
|
|
@ -38,6 +38,7 @@ type DB interface {
|
|||
Notification
|
||||
Relationship
|
||||
Report
|
||||
Rule
|
||||
Search
|
||||
Session
|
||||
Status
|
||||
|
|
42
internal/db/rule.go
Normal file
42
internal/db/rule.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
30
internal/gtsmodel/rule.go
Normal 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
|
||||
}
|
127
internal/processing/admin/rule.go
Normal file
127
internal/processing/admin/rule.go
Normal 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 := >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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
>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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -141,22 +141,29 @@ function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) {
|
|||
{...disabledForm}
|
||||
/>
|
||||
|
||||
<MutationButton
|
||||
label="Suspend"
|
||||
result={addResult}
|
||||
{...disabledForm}
|
||||
/>
|
||||
|
||||
{
|
||||
isExistingBlock &&
|
||||
<div className="action-buttons row">
|
||||
<MutationButton
|
||||
type="button"
|
||||
onClick={() => removeBlock(block.id)}
|
||||
label="Remove"
|
||||
result={removeResult}
|
||||
className="button danger"
|
||||
label="Suspend"
|
||||
result={addResult}
|
||||
showError={false}
|
||||
{...disabledForm}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingBlock &&
|
||||
<MutationButton
|
||||
type="button"
|
||||
onClick={() => removeBlock(block.id)}
|
||||
label="Remove"
|
||||
result={removeResult}
|
||||
className="button danger"
|
||||
showError={false}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{addResult.error && <Error error={addResult.error} />}
|
||||
{removeResult.error && <Error error={removeResult.error} />}
|
||||
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -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 (
|
169
web/source/settings/admin/settings/rules.jsx
Normal file
169
web/source/settings/admin/settings/rules.jsx
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"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 (
|
||||
<FormWithData
|
||||
dataQuery={query.useInstanceRulesQuery}
|
||||
DataForm={InstanceRules}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function InstanceRules({ baseUrl, data: rules }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${baseUrl}/:ruleId`}>
|
||||
<InstanceRuleDetail rules={rules} />
|
||||
</Route>
|
||||
<Route>
|
||||
<div>
|
||||
<h1>Instance Rules</h1>
|
||||
<div>
|
||||
<p>
|
||||
The rules for your instance are listed on the about page, and can be selected when submitting reports.
|
||||
</p>
|
||||
</div>
|
||||
<InstanceRuleList rules={rules} />
|
||||
</div>
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function InstanceRuleList({ rules }) {
|
||||
const newRule = useTextInput("text", {});
|
||||
|
||||
const [submitForm, result] = useFormSubmit({ newRule }, query.useAddInstanceRuleMutation(), {
|
||||
onFinish: () => newRule.reset()
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={submitForm} className="new-rule">
|
||||
<ol className="instance-rules">
|
||||
{Object.values(rules).map((rule) => (
|
||||
<InstanceRule key={rule.id} rule={rule} />
|
||||
))}
|
||||
</ol>
|
||||
<TextArea
|
||||
field={newRule}
|
||||
label="New instance rule"
|
||||
/>
|
||||
<MutationButton label="Add rule" result={result} />
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InstanceRule({ rule }) {
|
||||
const baseUrl = useBaseUrl();
|
||||
|
||||
return (
|
||||
<Link to={`${baseUrl}/${rule.id}`}>
|
||||
<a className="rule">
|
||||
<li>
|
||||
<h2>{rule.text} <i className="fa fa-pencil edit-icon" /></h2>
|
||||
</li>
|
||||
<span>{new Date(rule.created_at).toLocaleString()}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function InstanceRuleDetail({ rules }) {
|
||||
const baseUrl = useBaseUrl();
|
||||
let [_match, params] = useRoute(`${baseUrl}/:ruleId`);
|
||||
|
||||
if (params?.ruleId == undefined || rules[params.ruleId] == undefined) {
|
||||
return <Redirect to={baseUrl} />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Link to={baseUrl}><a>< go back</a></Link>
|
||||
<InstanceRuleForm rule={rules[params.ruleId]} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Redirect to={baseUrl} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rule-detail">
|
||||
<form onSubmit={submitForm}>
|
||||
<TextArea
|
||||
field={form.rule}
|
||||
/>
|
||||
|
||||
<div className="action-buttons row">
|
||||
<MutationButton
|
||||
label="Save"
|
||||
showError={false}
|
||||
result={result}
|
||||
disabled={!form.rule.hasChanged()}
|
||||
/>
|
||||
|
||||
<MutationButton
|
||||
type="button"
|
||||
onClick={() => deleteRule(rule.id)}
|
||||
label="Delete"
|
||||
className="button danger"
|
||||
showError={false}
|
||||
result={deleteResult}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{result.error && <Error error={result.error} />}
|
||||
{deleteResult.error && <Error error={deleteResult.error} />}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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"))
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: () => ({
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Admin Contact</h2>
|
||||
<h2 id="contact">Admin Contact</h2>
|
||||
{{if .instance.ContactAccount}}
|
||||
<a href="{{.instance.ContactAccount.URL}}" class="account-card">
|
||||
<img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
|
||||
|
@ -42,7 +42,16 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Features</h2>
|
||||
<h2 id="rules">Rules</h2>
|
||||
<ol>
|
||||
{{range .instance.Rules}}
|
||||
<li>{{.Text}}</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 id="features">Features</h2>
|
||||
<ul>
|
||||
<li>
|
||||
Registration is
|
||||
|
@ -68,8 +77,9 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Moderated servers</h2>
|
||||
<h2 id="moderated-servers">Moderated servers</h2>
|
||||
<p>
|
||||
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 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Instance Statistics</h2>
|
||||
<ul>
|
||||
<li>Users: <span class="count">{{.instance.Stats.user_count}}</span></li>
|
||||
<li>Posts: <span class="count">{{.instance.Stats.status_count}}</span></li>
|
||||
<li>Federates with: <span class="count">{{.instance.Stats.domain_count}}</span> instances</li>
|
||||
</ul>
|
||||
<h2 id="stats">Instance Statistics</h2>
|
||||
<ul>
|
||||
<li>Users: <span class="count">{{.instance.Stats.user_count}}</span></li>
|
||||
<li>Posts: <span class="count">{{.instance.Stats.status_count}}</span></li>
|
||||
<li>Federates with: <span class="count">{{.instance.Stats.domain_count}}</span> instances</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
Loading…
Reference in a new issue