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
|
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||||
type: string
|
type: string
|
||||||
x-go-name: ID
|
x-go-name: ID
|
||||||
rule_ids:
|
rules:
|
||||||
description: |-
|
description: |-
|
||||||
Array of rule IDs that were submitted along with this report.
|
Array of rules that were broken according to this report.
|
||||||
NOT IMPLEMENTED, will always be empty array.
|
Will be empty if no rule IDs were submitted with the report.
|
||||||
items: {}
|
items:
|
||||||
|
$ref: '#/definitions/instanceRule'
|
||||||
type: array
|
type: array
|
||||||
x-go-name: Rules
|
x-go-name: Rules
|
||||||
statuses:
|
statuses:
|
||||||
|
@ -1274,6 +1275,36 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
x-go-name: InstanceConfigurationStatuses
|
x-go-name: InstanceConfigurationStatuses
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
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:
|
instanceV1:
|
||||||
properties:
|
properties:
|
||||||
account_domain:
|
account_domain:
|
||||||
|
@ -1330,6 +1361,12 @@ definitions:
|
||||||
description: New account registrations are enabled on this instance.
|
description: New account registrations are enabled on this instance.
|
||||||
type: boolean
|
type: boolean
|
||||||
x-go-name: Registrations
|
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:
|
short_description:
|
||||||
description: |-
|
description: |-
|
||||||
A shorter description of the instance.
|
A shorter description of the instance.
|
||||||
|
@ -1453,10 +1490,9 @@ definitions:
|
||||||
registrations:
|
registrations:
|
||||||
$ref: '#/definitions/instanceV2Registrations'
|
$ref: '#/definitions/instanceV2Registrations'
|
||||||
rules:
|
rules:
|
||||||
description: |-
|
description: An itemized list of rules for this instance.
|
||||||
An itemized list of rules for this website.
|
items:
|
||||||
Currently not implemented (will always be empty array).
|
$ref: '#/definitions/instanceRule'
|
||||||
items: {}
|
|
||||||
type: array
|
type: array
|
||||||
x-go-name: Rules
|
x-go-name: Rules
|
||||||
source_url:
|
source_url:
|
||||||
|
@ -1755,6 +1791,72 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
x-go-name: MediaMeta
|
x-go-name: MediaMeta
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
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:
|
nodeinfo:
|
||||||
description: 'See: https://nodeinfo.diaspora.software/schema.html'
|
description: 'See: https://nodeinfo.diaspora.software/schema.html'
|
||||||
properties:
|
properties:
|
||||||
|
@ -1971,11 +2073,10 @@ definitions:
|
||||||
Array of rule IDs that were submitted along with this report.
|
Array of rule IDs that were submitted along with this report.
|
||||||
Will be empty if no rule IDs were submitted.
|
Will be empty if no rule IDs were submitted.
|
||||||
example:
|
example:
|
||||||
- 1
|
- 01GPBN5YDY6JKBWE44H7YQBDCQ
|
||||||
- 2
|
- 01GPBN65PDWSBPWVDD0SQCFFY3
|
||||||
items:
|
items:
|
||||||
format: int64
|
type: string
|
||||||
type: integer
|
|
||||||
type: array
|
type: array
|
||||||
x-go-name: RuleIDs
|
x-go-name: RuleIDs
|
||||||
status_ids:
|
status_ids:
|
||||||
|
@ -4036,6 +4137,118 @@ paths:
|
||||||
summary: Send a generic test email to a specified email address.
|
summary: Send a generic test email to a specified email address.
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- 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:
|
/api/v1/admin/media_cleanup:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
@ -4251,6 +4464,67 @@ paths:
|
||||||
summary: Mark a report as resolved.
|
summary: Mark a report as resolved.
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- 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:
|
/api/v1/apps:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
@ -4750,6 +5024,30 @@ paths:
|
||||||
description: internal server error
|
description: internal server error
|
||||||
tags:
|
tags:
|
||||||
- instance
|
- 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:
|
/api/v1/lists:
|
||||||
get:
|
get:
|
||||||
operationId: lists
|
operationId: lists
|
||||||
|
@ -5505,17 +5803,13 @@ paths:
|
||||||
name: category
|
name: category
|
||||||
type: string
|
type: string
|
||||||
x-go-name: Category
|
x-go-name: Category
|
||||||
- description: |-
|
- description: IDs of rules on this instance which have been broken according to the reporter.
|
||||||
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:
|
example:
|
||||||
- 1
|
- 01GPBN5YDY6JKBWE44H7YQBDCQ
|
||||||
- 2
|
- 01GPBN65PDWSBPWVDD0SQCFFY3
|
||||||
- 3
|
|
||||||
in: formData
|
in: formData
|
||||||
items:
|
items:
|
||||||
format: int64
|
type: string
|
||||||
type: integer
|
|
||||||
name: rule_ids
|
name: rule_ids
|
||||||
type: array
|
type: array
|
||||||
x-go-name: RuleIDs
|
x-go-name: RuleIDs
|
||||||
|
|
|
@ -25,22 +25,24 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BasePath = "/v1/admin"
|
BasePath = "/v1/admin"
|
||||||
EmojiPath = BasePath + "/custom_emojis"
|
EmojiPath = BasePath + "/custom_emojis"
|
||||||
EmojiPathWithID = EmojiPath + "/:" + IDKey
|
EmojiPathWithID = EmojiPath + "/:" + IDKey
|
||||||
EmojiCategoriesPath = EmojiPath + "/categories"
|
EmojiCategoriesPath = EmojiPath + "/categories"
|
||||||
DomainBlocksPath = BasePath + "/domain_blocks"
|
DomainBlocksPath = BasePath + "/domain_blocks"
|
||||||
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
|
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
|
||||||
AccountsPath = BasePath + "/accounts"
|
AccountsPath = BasePath + "/accounts"
|
||||||
AccountsPathWithID = AccountsPath + "/:" + IDKey
|
AccountsPathWithID = AccountsPath + "/:" + IDKey
|
||||||
AccountsActionPath = AccountsPathWithID + "/action"
|
AccountsActionPath = AccountsPathWithID + "/action"
|
||||||
MediaCleanupPath = BasePath + "/media_cleanup"
|
MediaCleanupPath = BasePath + "/media_cleanup"
|
||||||
MediaRefetchPath = BasePath + "/media_refetch"
|
MediaRefetchPath = BasePath + "/media_refetch"
|
||||||
ReportsPath = BasePath + "/reports"
|
ReportsPath = BasePath + "/reports"
|
||||||
ReportsPathWithID = ReportsPath + "/:" + IDKey
|
ReportsPathWithID = ReportsPath + "/:" + IDKey
|
||||||
ReportsResolvePath = ReportsPathWithID + "/resolve"
|
ReportsResolvePath = ReportsPathWithID + "/resolve"
|
||||||
EmailPath = BasePath + "/email"
|
EmailPath = BasePath + "/email"
|
||||||
EmailTestPath = EmailPath + "/test"
|
EmailTestPath = EmailPath + "/test"
|
||||||
|
InstanceRulesPath = BasePath + "/instance/rules"
|
||||||
|
InstanceRulesPathWithID = InstanceRulesPath + "/:" + IDKey
|
||||||
|
|
||||||
IDKey = "id"
|
IDKey = "id"
|
||||||
FilterQueryKey = "filter"
|
FilterQueryKey = "filter"
|
||||||
|
@ -95,4 +97,11 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
||||||
|
|
||||||
// email stuff
|
// email stuff
|
||||||
attachHandler(http.MethodPost, EmailTestPath, m.EmailTestPOSTHandler)
|
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"
|
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
|
||||||
},
|
},
|
||||||
"statuses": [],
|
"statuses": [],
|
||||||
"rule_ids": [],
|
"rules": [],
|
||||||
"action_taken_comment": "user was warned not to be a turtle anymore"
|
"action_taken_comment": "user was warned not to be a turtle anymore"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -528,7 +528,16 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
||||||
"poll": null
|
"poll": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
],
|
||||||
"action_taken_comment": null
|
"action_taken_comment": null
|
||||||
}
|
}
|
||||||
]`, string(b))
|
]`, string(b))
|
||||||
|
@ -740,7 +749,16 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
||||||
"poll": null
|
"poll": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
],
|
||||||
"action_taken_comment": null
|
"action_taken_comment": null
|
||||||
}
|
}
|
||||||
]`, string(b))
|
]`, string(b))
|
||||||
|
@ -952,7 +970,16 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
||||||
"poll": null
|
"poll": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
],
|
||||||
"action_taken_comment": null
|
"action_taken_comment": null
|
||||||
}
|
}
|
||||||
]`, string(b))
|
]`, 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"
|
InstanceInformationPathV1 = "/v1/instance"
|
||||||
InstanceInformationPathV2 = "/v2/instance"
|
InstanceInformationPathV2 = "/v2/instance"
|
||||||
InstancePeersPath = InstanceInformationPathV1 + "/peers"
|
InstancePeersPath = InstanceInformationPathV1 + "/peers"
|
||||||
|
InstanceRulesPath = InstanceInformationPathV1 + "/rules"
|
||||||
PeersFilterKey = "filter" // PeersFilterKey is used to provide filters to /api/v1/instance/peers
|
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.MethodPatch, InstanceInformationPathV1, m.InstanceUpdatePATCHHandler)
|
||||||
attachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler)
|
attachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler)
|
||||||
|
|
||||||
|
attachHandler(http.MethodGet, InstanceRulesPath, m.InstanceRulesGETHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,7 +160,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
||||||
"name": "admin"
|
"name": "admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"max_toot_chars": 5000
|
"max_toot_chars": 5000,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
]
|
||||||
}`, dst.String())
|
}`, dst.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +274,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
||||||
"name": "admin"
|
"name": "admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"max_toot_chars": 5000
|
"max_toot_chars": 5000,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
]
|
||||||
}`, dst.String())
|
}`, dst.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -368,7 +388,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
||||||
"name": "admin"
|
"name": "admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"max_toot_chars": 5000
|
"max_toot_chars": 5000,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
]
|
||||||
}`, dst.String())
|
}`, dst.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -523,7 +553,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
||||||
"name": "admin"
|
"name": "admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"max_toot_chars": 5000
|
"max_toot_chars": 5000,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
]
|
||||||
}`, dst.String())
|
}`, dst.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -651,7 +691,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
||||||
"name": "admin"
|
"name": "admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"max_toot_chars": 5000
|
"max_toot_chars": 5000,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
]
|
||||||
}`, dst.String())
|
}`, dst.String())
|
||||||
|
|
||||||
// extra bonus: check the v2 model thumbnail after the patch
|
// extra bonus: check the v2 model thumbnail after the patch
|
||||||
|
@ -790,7 +840,17 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
||||||
"name": "admin"
|
"name": "admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"max_toot_chars": 5000
|
"max_toot_chars": 5000,
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
]
|
||||||
}`, dst.String())
|
}`, 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
|
// create the request
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+reports.BasePath, nil)
|
ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+reports.BasePath, nil)
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
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{
|
ctx.Request.Form = url.Values{
|
||||||
"account_id": {form.AccountID},
|
"account_id": {form.AccountID},
|
||||||
"status_ids[]": form.StatusIDs,
|
"status_ids[]": form.StatusIDs,
|
||||||
"comment": {form.Comment},
|
"comment": {form.Comment},
|
||||||
"forward": {strconv.FormatBool(form.Forward)},
|
"forward": {strconv.FormatBool(form.Forward)},
|
||||||
"category": {form.Category},
|
"category": {form.Category},
|
||||||
"rule_ids[]": ruleIDs,
|
"rule_ids[]": form.RuleIDs,
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger the handler
|
// trigger the handler
|
||||||
|
|
|
@ -108,7 +108,10 @@ func (suite *ReportGetTestSuite) TestGetReport1() {
|
||||||
"status_ids": [
|
"status_ids": [
|
||||||
"01FVW7JHQFSFK166WWKR8CBA6M"
|
"01FVW7JHQFSFK166WWKR8CBA6M"
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rule_ids": [
|
||||||
|
"01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
|
||||||
|
],
|
||||||
"target_account": {
|
"target_account": {
|
||||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||||
"username": "foss_satan",
|
"username": "foss_satan",
|
||||||
|
|
|
@ -133,7 +133,10 @@ func (suite *ReportsGetTestSuite) TestGetReports() {
|
||||||
"status_ids": [
|
"status_ids": [
|
||||||
"01FVW7JHQFSFK166WWKR8CBA6M"
|
"01FVW7JHQFSFK166WWKR8CBA6M"
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rule_ids": [
|
||||||
|
"01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
|
||||||
|
],
|
||||||
"target_account": {
|
"target_account": {
|
||||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||||
"username": "foss_satan",
|
"username": "foss_satan",
|
||||||
|
@ -220,7 +223,10 @@ func (suite *ReportsGetTestSuite) TestGetReports4() {
|
||||||
"status_ids": [
|
"status_ids": [
|
||||||
"01FVW7JHQFSFK166WWKR8CBA6M"
|
"01FVW7JHQFSFK166WWKR8CBA6M"
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rule_ids": [
|
||||||
|
"01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
|
||||||
|
],
|
||||||
"target_account": {
|
"target_account": {
|
||||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||||
"username": "foss_satan",
|
"username": "foss_satan",
|
||||||
|
@ -291,7 +297,10 @@ func (suite *ReportsGetTestSuite) TestGetReports6() {
|
||||||
"status_ids": [
|
"status_ids": [
|
||||||
"01FVW7JHQFSFK166WWKR8CBA6M"
|
"01FVW7JHQFSFK166WWKR8CBA6M"
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rule_ids": [
|
||||||
|
"01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
|
||||||
|
],
|
||||||
"target_account": {
|
"target_account": {
|
||||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||||
"username": "foss_satan",
|
"username": "foss_satan",
|
||||||
|
@ -346,7 +355,10 @@ func (suite *ReportsGetTestSuite) TestGetReports7() {
|
||||||
"status_ids": [
|
"status_ids": [
|
||||||
"01FVW7JHQFSFK166WWKR8CBA6M"
|
"01FVW7JHQFSFK166WWKR8CBA6M"
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rule_ids": [
|
||||||
|
"01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
|
||||||
|
],
|
||||||
"target_account": {
|
"target_account": {
|
||||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||||
"username": "foss_satan",
|
"username": "foss_satan",
|
||||||
|
|
|
@ -117,9 +117,9 @@ type AdminReport struct {
|
||||||
// Array of statuses that were submitted along with this report.
|
// Array of statuses that were submitted along with this report.
|
||||||
// Will be empty if no status IDs were submitted with the report.
|
// Will be empty if no status IDs were submitted with the report.
|
||||||
Statuses []*Status `json:"statuses"`
|
Statuses []*Status `json:"statuses"`
|
||||||
// Array of rule IDs that were submitted along with this report.
|
// Array of rules that were broken according to this report.
|
||||||
// NOT IMPLEMENTED, will always be empty array.
|
// Will be empty if no rule IDs were submitted with the report.
|
||||||
Rules []interface{} `json:"rule_ids"`
|
Rules []*InstanceRule `json:"rules"`
|
||||||
// If an action was taken, what comment was made by the admin on the taken action?
|
// 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.
|
// Will be null if not set / no action yet taken.
|
||||||
// example: Account was suspended.
|
// example: Account was suspended.
|
||||||
|
@ -189,3 +189,10 @@ type AdminSendTestEmailRequest struct {
|
||||||
// Email address to send the test email to.
|
// Email address to send the test email to.
|
||||||
Email string `form:"email" json:"email" xml:"email"`
|
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
|
// example: 5000
|
||||||
MaxTootChars uint `json:"max_toot_chars"`
|
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.
|
// InstanceV1URLs models instance-relevant URLs for client application consumption.
|
||||||
|
|
|
@ -62,9 +62,8 @@ type InstanceV2 struct {
|
||||||
Registrations InstanceV2Registrations `json:"registrations"`
|
Registrations InstanceV2Registrations `json:"registrations"`
|
||||||
// Hints related to contacting a representative of the instance.
|
// Hints related to contacting a representative of the instance.
|
||||||
Contact InstanceV2Contact `json:"contact"`
|
Contact InstanceV2Contact `json:"contact"`
|
||||||
// An itemized list of rules for this website.
|
// An itemized list of rules for this instance.
|
||||||
// Currently not implemented (will always be empty array).
|
Rules []InstanceRule `json:"rules"`
|
||||||
Rules []interface{} `json:"rules"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage data for this instance.
|
// Usage data for this instance.
|
||||||
|
|
|
@ -54,8 +54,8 @@ type Report struct {
|
||||||
StatusIDs []string `json:"status_ids"`
|
StatusIDs []string `json:"status_ids"`
|
||||||
// Array of rule IDs that were submitted along with this report.
|
// Array of rule IDs that were submitted along with this report.
|
||||||
// Will be empty if no rule IDs were submitted.
|
// Will be empty if no rule IDs were submitted.
|
||||||
// example: [1, 2]
|
// example: ["01GPBN5YDY6JKBWE44H7YQBDCQ","01GPBN65PDWSBPWVDD0SQCFFY3"]
|
||||||
RuleIDs []int `json:"rule_ids"`
|
RuleIDs []string `json:"rule_ids"`
|
||||||
// Account that was reported.
|
// Account that was reported.
|
||||||
TargetAccount *Account `json:"target_account"`
|
TargetAccount *Account `json:"target_account"`
|
||||||
}
|
}
|
||||||
|
@ -89,8 +89,7 @@ type ReportCreateRequest struct {
|
||||||
// in: formData
|
// in: formData
|
||||||
Category string `form:"category" json:"category" xml:"category"`
|
Category string `form:"category" json:"category" xml:"category"`
|
||||||
// IDs of rules on this instance which have been broken according to the reporter.
|
// 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: ["01GPBN5YDY6JKBWE44H7YQBDCQ","01GPBN65PDWSBPWVDD0SQCFFY3"]
|
||||||
// example: [1, 2, 3]
|
|
||||||
// in: formData
|
// 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.Notification
|
||||||
db.Relationship
|
db.Relationship
|
||||||
db.Report
|
db.Report
|
||||||
|
db.Rule
|
||||||
db.Search
|
db.Search
|
||||||
db.Session
|
db.Session
|
||||||
db.Status
|
db.Status
|
||||||
|
@ -216,6 +217,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
||||||
db: db,
|
db: db,
|
||||||
state: state,
|
state: state,
|
||||||
},
|
},
|
||||||
|
Rule: &ruleDB{
|
||||||
|
db: db,
|
||||||
|
state: state,
|
||||||
|
},
|
||||||
Search: &searchDB{
|
Search: &searchDB{
|
||||||
db: db,
|
db: db,
|
||||||
state: state,
|
state: state,
|
||||||
|
|
|
@ -51,6 +51,7 @@ type BunDBStandardTestSuite struct {
|
||||||
testListEntries map[string]*gtsmodel.ListEntry
|
testListEntries map[string]*gtsmodel.ListEntry
|
||||||
testAccountNotes map[string]*gtsmodel.AccountNote
|
testAccountNotes map[string]*gtsmodel.AccountNote
|
||||||
testMarkers map[string]*gtsmodel.Marker
|
testMarkers map[string]*gtsmodel.Marker
|
||||||
|
testRules map[string]*gtsmodel.Rule
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
|
@ -72,6 +73,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
suite.testListEntries = testrig.NewTestListEntries()
|
suite.testListEntries = testrig.NewTestListEntries()
|
||||||
suite.testAccountNotes = testrig.NewTestAccountNotes()
|
suite.testAccountNotes = testrig.NewTestAccountNotes()
|
||||||
suite.testMarkers = testrig.NewTestMarkers()
|
suite.testMarkers = testrig.NewTestMarkers()
|
||||||
|
suite.testRules = testrig.NewTestRules()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||||
|
|
|
@ -151,6 +151,16 @@ func (i *instanceDB) getInstance(ctx context.Context, lookup string, dbQuery fun
|
||||||
return nil, err
|
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
|
return &instance, nil
|
||||||
}, keyParts...)
|
}, keyParts...)
|
||||||
if err != nil {
|
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 != "" &&
|
if report.ActionTakenByAccountID != "" &&
|
||||||
report.ActionTakenByAccount == nil {
|
report.ActionTakenByAccount == nil {
|
||||||
// Report action account is not set, fetch from the database.
|
// 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
|
Notification
|
||||||
Relationship
|
Relationship
|
||||||
Report
|
Report
|
||||||
|
Rule
|
||||||
Search
|
Search
|
||||||
Session
|
Session
|
||||||
Status
|
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
|
ContactAccount *Account `bun:"rel:belongs-to"` // account corresponding to contactAccountID
|
||||||
Reputation int64 `bun:",notnull,default:0"` // Reputation score of this instance
|
Reputation int64 `bun:",notnull,default:0"` // Reputation score of this instance
|
||||||
Version string `bun:",nullzero"` // Version of the software used on 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
|
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
|
StatusIDs []string `bun:"statuses,array"` // database IDs of any statuses referenced by this report
|
||||||
Statuses []*Status `bun:"-"` // statuses corresponding to StatusIDs
|
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
|
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
|
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
|
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
|
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) {
|
func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||||
// fetch the instance entry from the db for processing
|
// fetch the instance entry from the db for processing
|
||||||
host := config.GetHost()
|
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()
|
reportID := id.NewULID()
|
||||||
report := >smodel.Report{
|
report := >smodel.Report{
|
||||||
ID: reportID,
|
ID: reportID,
|
||||||
|
@ -75,6 +82,8 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
|
||||||
Comment: form.Comment,
|
Comment: form.Comment,
|
||||||
StatusIDs: form.StatusIDs,
|
StatusIDs: form.StatusIDs,
|
||||||
Statuses: statuses,
|
Statuses: statuses,
|
||||||
|
RuleIDs: form.RuleIDs,
|
||||||
|
Rules: rules,
|
||||||
Forwarded: &form.Forward,
|
Forwarded: &form.Forward,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,10 @@ type TypeConverter interface {
|
||||||
InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error)
|
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 converts a gts instance into its api equivalent for serving at /api/v2/instance
|
||||||
InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV2, error)
|
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 converts a gts relationship into its api equivalent for serving in various places
|
||||||
RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error)
|
RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error)
|
||||||
// NotificationToAPINotification converts a gts notification into a api notification
|
// 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 ""
|
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) {
|
func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error) {
|
||||||
instance := &apimodel.InstanceV1{
|
instance := &apimodel.InstanceV1{
|
||||||
URI: i.URI,
|
URI: i.URI,
|
||||||
|
@ -752,6 +778,7 @@ func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
|
||||||
ApprovalRequired: config.GetAccountsApprovalRequired(),
|
ApprovalRequired: config.GetAccountsApprovalRequired(),
|
||||||
InvitesEnabled: false, // todo: not supported yet
|
InvitesEnabled: false, // todo: not supported yet
|
||||||
MaxTootChars: uint(config.GetStatusesMaxChars()),
|
MaxTootChars: uint(config.GetStatusesMaxChars()),
|
||||||
|
Rules: c.InstanceRulesToAPIRules(i.Rules),
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.GetInstanceInjectMastodonVersion() {
|
if config.GetInstanceInjectMastodonVersion() {
|
||||||
|
@ -854,7 +881,7 @@ func (c *converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
|
||||||
Description: i.Description,
|
Description: i.Description,
|
||||||
Usage: apimodel.InstanceV2Usage{}, // todo: not implemented
|
Usage: apimodel.InstanceV2Usage{}, // todo: not implemented
|
||||||
Languages: []string{}, // todo: not implemented
|
Languages: []string{}, // todo: not implemented
|
||||||
Rules: []interface{}{}, // todo: not implemented
|
Rules: c.InstanceRulesToAPIRules(i.Rules),
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.GetInstanceInjectMastodonVersion() {
|
if config.GetInstanceInjectMastodonVersion() {
|
||||||
|
@ -1051,7 +1078,7 @@ func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (
|
||||||
Comment: r.Comment,
|
Comment: r.Comment,
|
||||||
Forwarded: *r.Forwarded,
|
Forwarded: *r.Forwarded,
|
||||||
StatusIDs: r.StatusIDs,
|
StatusIDs: r.StatusIDs,
|
||||||
RuleIDs: []int{}, // todo: not supported yet
|
RuleIDs: r.RuleIDs,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.ActionTakenAt.IsZero() {
|
if !r.ActionTakenAt.IsZero() {
|
||||||
|
@ -1144,6 +1171,20 @@ func (c *converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
|
||||||
statuses = append(statuses, status)
|
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 != "" {
|
if ac := r.ActionTaken; ac != "" {
|
||||||
actionTakenComment = &ac
|
actionTakenComment = &ac
|
||||||
}
|
}
|
||||||
|
@ -1163,7 +1204,7 @@ func (c *converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
|
||||||
ActionTakenByAccount: actionTakenByAccount,
|
ActionTakenByAccount: actionTakenByAccount,
|
||||||
ActionTakenComment: actionTakenComment,
|
ActionTakenComment: actionTakenComment,
|
||||||
Statuses: statuses,
|
Statuses: statuses,
|
||||||
Rules: []interface{}{}, // not implemented
|
Rules: rules,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -603,6 +603,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
|
||||||
b, err := json.MarshalIndent(instance, "", " ")
|
b, err := json.MarshalIndent(instance, "", " ")
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// FIXME: "rules" is empty from the database, because it's not fetched through db.GetInstance
|
||||||
suite.Equal(`{
|
suite.Equal(`{
|
||||||
"uri": "http://localhost:8080",
|
"uri": "http://localhost:8080",
|
||||||
"account_domain": "localhost:8080",
|
"account_domain": "localhost:8080",
|
||||||
|
@ -689,7 +690,8 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
|
||||||
"name": "admin"
|
"name": "admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"max_toot_chars": 5000
|
"max_toot_chars": 5000,
|
||||||
|
"rules": []
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -887,7 +889,10 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend1() {
|
||||||
"status_ids": [
|
"status_ids": [
|
||||||
"01FVW7JHQFSFK166WWKR8CBA6M"
|
"01FVW7JHQFSFK166WWKR8CBA6M"
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rule_ids": [
|
||||||
|
"01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"01GP3DFY9XQ1TJMZT5BGAZPXX3"
|
||||||
|
],
|
||||||
"target_account": {
|
"target_account": {
|
||||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||||
"username": "foss_satan",
|
"username": "foss_satan",
|
||||||
|
@ -1177,7 +1182,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
||||||
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
|
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
|
||||||
},
|
},
|
||||||
"statuses": [],
|
"statuses": [],
|
||||||
"rule_ids": [],
|
"rules": [],
|
||||||
"action_taken_comment": "user was warned not to be a turtle anymore"
|
"action_taken_comment": "user was warned not to be a turtle anymore"
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
@ -1380,7 +1385,16 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
||||||
"poll": null
|
"poll": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rule_ids": [],
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "01GP3AWY4CRDVRNZKW0TEAMB51",
|
||||||
|
"text": "Be gay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01GP3DFY9XQ1TJMZT5BGAZPXX3",
|
||||||
|
"text": "Do crime"
|
||||||
|
}
|
||||||
|
],
|
||||||
"action_taken_comment": null
|
"action_taken_comment": null
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
@ -1603,7 +1617,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
||||||
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
|
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
|
||||||
},
|
},
|
||||||
"statuses": [],
|
"statuses": [],
|
||||||
"rule_ids": [],
|
"rules": [],
|
||||||
"action_taken_comment": "user was warned not to be a turtle anymore"
|
"action_taken_comment": "user was warned not to be a turtle anymore"
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
>smodel.EmojiCategory{},
|
>smodel.EmojiCategory{},
|
||||||
>smodel.Tombstone{},
|
>smodel.Tombstone{},
|
||||||
>smodel.Report{},
|
>smodel.Report{},
|
||||||
|
>smodel.Rule{},
|
||||||
>smodel.AccountNote{},
|
>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() {
|
for _, v := range NewTestDomainBlocks() {
|
||||||
if err := db.Put(ctx, v); err != nil {
|
if err := db.Put(ctx, v); err != nil {
|
||||||
log.Panic(nil, err)
|
log.Panic(nil, err)
|
||||||
|
|
|
@ -2021,6 +2021,7 @@ func NewTestReports() map[string]*gtsmodel.Report {
|
||||||
Comment: "dark souls sucks, please yeet this nerd",
|
Comment: "dark souls sucks, please yeet this nerd",
|
||||||
StatusIDs: []string{"01FVW7JHQFSFK166WWKR8CBA6M"},
|
StatusIDs: []string{"01FVW7JHQFSFK166WWKR8CBA6M"},
|
||||||
Forwarded: util.Ptr(true),
|
Forwarded: util.Ptr(true),
|
||||||
|
RuleIDs: []string{"01GP3AWY4CRDVRNZKW0TEAMB51", "01GP3DFY9XQ1TJMZT5BGAZPXX3"},
|
||||||
},
|
},
|
||||||
"remote_account_1_report_local_account_2": {
|
"remote_account_1_report_local_account_2": {
|
||||||
ID: "01GP3DFY9XQ1TJMZT5BGAZPXX7",
|
ID: "01GP3DFY9XQ1TJMZT5BGAZPXX7",
|
||||||
|
@ -2031,6 +2032,7 @@ func NewTestReports() map[string]*gtsmodel.Report {
|
||||||
TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
Comment: "this is a turtle, not a person, therefore should not be a poster",
|
Comment: "this is a turtle, not a person, therefore should not be a poster",
|
||||||
StatusIDs: []string{},
|
StatusIDs: []string{},
|
||||||
|
RuleIDs: []string{},
|
||||||
Forwarded: util.Ptr(true),
|
Forwarded: util.Ptr(true),
|
||||||
ActionTaken: "user was warned not to be a turtle anymore",
|
ActionTaken: "user was warned not to be a turtle anymore",
|
||||||
ActionTakenAt: TimeMustParse("2022-05-15T17:01:56+02:00"),
|
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.
|
// ActivityWithSignature wraps a pub.Activity along with its signature headers, for testing.
|
||||||
type ActivityWithSignature struct {
|
type ActivityWithSignature struct {
|
||||||
Activity pub.Activity
|
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) {
|
@media screen and (max-width: 30rem) {
|
||||||
.domain-blocklist .entry {
|
.domain-blocklist .entry {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
@ -141,22 +141,29 @@ function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) {
|
||||||
{...disabledForm}
|
{...disabledForm}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MutationButton
|
<div className="action-buttons row">
|
||||||
label="Suspend"
|
|
||||||
result={addResult}
|
|
||||||
{...disabledForm}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
isExistingBlock &&
|
|
||||||
<MutationButton
|
<MutationButton
|
||||||
type="button"
|
label="Suspend"
|
||||||
onClick={() => removeBlock(block.id)}
|
result={addResult}
|
||||||
label="Remove"
|
showError={false}
|
||||||
result={removeResult}
|
{...disabledForm}
|
||||||
className="button danger"
|
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
|
{
|
||||||
|
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>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,23 +21,23 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../lib/query");
|
const query = require("../../lib/query");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useFileInput
|
useFileInput
|
||||||
} = require("../lib/form");
|
} = require("../../lib/form");
|
||||||
|
|
||||||
const useFormSubmit = require("../lib/form/submit");
|
const useFormSubmit = require("../../lib/form/submit");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TextInput,
|
TextInput,
|
||||||
TextArea,
|
TextArea,
|
||||||
FileInput
|
FileInput
|
||||||
} = require("../components/form/inputs");
|
} = require("../../components/form/inputs");
|
||||||
|
|
||||||
const FormWithData = require("../lib/form/form-with-data");
|
const FormWithData = require("../../lib/form/form-with-data");
|
||||||
const MutationButton = require("../components/form/mutation-button");
|
const MutationButton = require("../../components/form/mutation-button");
|
||||||
|
|
||||||
module.exports = function AdminSettings() {
|
module.exports = function AdminSettings() {
|
||||||
return (
|
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("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),
|
||||||
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
|
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 {
|
const {
|
||||||
replaceCacheOnMutation,
|
replaceCacheOnMutation,
|
||||||
removeFromCacheOnMutation,
|
removeFromCacheOnMutation,
|
||||||
domainListToObject
|
domainListToObject,
|
||||||
|
idListToObject
|
||||||
} = require("../lib");
|
} = require("../lib");
|
||||||
const base = require("../base");
|
const base = require("../base");
|
||||||
|
|
||||||
|
@ -104,6 +105,51 @@ const endpoints = (build) => ({
|
||||||
return res.accounts ?? [];
|
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("./import-export")(build),
|
||||||
...require("./custom-emoji")(build),
|
...require("./custom-emoji")(build),
|
||||||
...require("./reports")(build)
|
...require("./reports")(build)
|
||||||
|
|
|
@ -59,7 +59,7 @@ function instanceBasedQuery(args, api, extraOptions) {
|
||||||
module.exports = createApi({
|
module.exports = createApi({
|
||||||
reducerPath: "api",
|
reducerPath: "api",
|
||||||
baseQuery: instanceBasedQuery,
|
baseQuery: instanceBasedQuery,
|
||||||
tagTypes: ["Auth", "Emoji", "Reports", "Account"],
|
tagTypes: ["Auth", "Emoji", "Reports", "Account", "InstanceRules"],
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
instance: build.query({
|
instance: build.query({
|
||||||
query: () => ({
|
query: () => ({
|
||||||
|
|
|
@ -37,6 +37,13 @@ module.exports = {
|
||||||
(_) => Object.fromEntries(_)
|
(_) => 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) => {
|
replaceCacheOnMutation: makeCacheMutation((draft, newData) => {
|
||||||
Object.assign(draft, newData);
|
Object.assign(draft, newData);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>Admin Contact</h2>
|
<h2 id="contact">Admin Contact</h2>
|
||||||
{{if .instance.ContactAccount}}
|
{{if .instance.ContactAccount}}
|
||||||
<a href="{{.instance.ContactAccount.URL}}" class="account-card">
|
<a href="{{.instance.ContactAccount.URL}}" class="account-card">
|
||||||
<img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
|
<img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
|
||||||
|
@ -42,7 +42,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Registration is
|
Registration is
|
||||||
|
@ -68,8 +77,9 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>Moderated servers</h2>
|
<h2 id="moderated-servers">Moderated servers</h2>
|
||||||
<p>
|
<p>
|
||||||
ActivityPub instances exchange (federate) data with other servers, including accounts and toots.
|
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,
|
This can be prevented for specific domains by suspending them. None of their content is stored,
|
||||||
|
@ -83,12 +93,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>Instance Statistics</h2>
|
<h2 id="stats">Instance Statistics</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Users: <span class="count">{{.instance.Stats.user_count}}</span></li>
|
<li>Users: <span class="count">{{.instance.Stats.user_count}}</span></li>
|
||||||
<li>Posts: <span class="count">{{.instance.Stats.status_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>
|
<li>Federates with: <span class="count">{{.instance.Stats.domain_count}}</span> instances</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
Loading…
Reference in a new issue