mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:22:25 -05: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 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 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 @@ const ( | ||||||
| 	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 @@ var testModels = []interface{}{ | ||||||
| 	>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…
	
	Add table
		Add a link
		
	
		Reference in a new issue