This commit is contained in:
tobi 2024-10-31 17:46:22 +01:00
parent 49eb8f602e
commit e62f5f9dbc
46 changed files with 4134 additions and 210 deletions

View file

@ -1124,6 +1124,73 @@ definitions:
type: object
x-go-name: DomainPermission
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
domainPermissionSubscription:
properties:
as_draft:
description: If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. If false, domain permissions from this subscription will come into force immediately.
example: true
type: boolean
x-go-name: AsDraft
content_type:
description: MIME content type to expect at URI.
example: text/csv
type: string
x-go-name: ContentType
count:
description: Count of domain permission entries discovered at URI.
example: 53
format: uint64
readOnly: true
type: integer
x-go-name: Count
created_by_account_id:
description: ID of the account that created this subscription.
example: 01FBW21XJA09XYX51KV5JVBW0F
readOnly: true
type: string
x-go-name: CreatedByAccountID
error:
description: If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
example: Oopsie doopsie, we made a fucky wucky.
readOnly: true
type: string
x-go-name: Error
fetch_password:
description: (Optional) password to set for basic auth when doing a fetch of URI.
example: admin123
type: string
x-go-name: FetchPassword
fetch_username:
description: (Optional) username to set for basic auth when doing a fetch of URI.
example: admin123
type: string
x-go-name: FetchUsername
fetched_at:
description: Time at which the most recent fetch was attempted (ISO 8601 Datetime).
example: "2021-07-30T09:20:25+00:00"
readOnly: true
type: string
x-go-name: FetchedAt
id:
description: The ID of the domain permission subscription.
example: 01FBW21XJA09XYX51KV5JVBW0F
readOnly: true
type: string
x-go-name: ID
permission_type:
description: The type of domain permission subscription (allow, block).
example: block
type: string
x-go-name: PermissionType
uri:
description: URI to call in order to fetch the permissions list.
example: https://www.example.org/blocklists/list1.csv
type: string
x-go-name: URI
title: DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
type: object
x-go-name: DomainPermissionSubscription
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
emoji:
properties:
category:
@ -5572,6 +5639,249 @@ paths:
summary: Force expiry of cached public keys for all accounts on the given domain stored in your database.
tags:
- admin
/api/v1/admin/domain_permission_drafts:
get:
description: |-
The drafts will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
The next and previous queries can be parsed from the returned Link header.
Example:
```
<https://example.org/api/v1/admin/domain_permission_drafts?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_drafts?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
````
operationId: domainPermissionDraftsGet
parameters:
- description: Show only drafts created by the given subscription ID.
in: query
name: subscription_id
type: string
- description: Return only drafts that target the given domain.
in: query
name: domain
type: string
- description: Filter on "block" or "allow" type drafts.
in: query
name: permission_type
type: string
- description: Return only items *OLDER* than the given max ID (for paging downwards). The item with the specified ID will not be included in the response.
in: query
name: max_id
type: string
- description: Return only items *NEWER* than the given since ID. The item with the specified ID will not be included in the response.
in: query
name: since_id
type: string
- description: Return only items immediately *NEWER* than the given min ID (for paging upwards). The item with the specified ID will not be included in the response.
in: query
name: min_id
type: string
- default: 20
description: Number of items to return.
in: query
maximum: 100
minimum: 1
name: limit
type: integer
produces:
- application/json
responses:
"200":
description: Domain permission drafts.
headers:
Link:
description: Links to the next and previous queries.
type: string
schema:
items:
$ref: '#/definitions/domainPermission'
type: array
"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: View domain permission drafts.
tags:
- admin
post:
consumes:
- multipart/form-data
- application/json
operationId: domainPermissionDraftCreate
parameters:
- description: Domain to create the permission draft for.
in: formData
name: domain
type: string
- description: Create a draft "allow" or a draft "block".
in: query
name: permission_type
type: string
- description: Obfuscate the name of the domain when serving it publicly. Eg., `example.org` becomes something like `ex***e.org`.
in: formData
name: obfuscate
type: boolean
- description: Public comment about this domain permission. This will be displayed alongside the domain permission if you choose to share permissions.
in: formData
name: public_comment
type: string
- description: Private comment about this domain permission. Will only be shown to other admins, so this is a useful way of internally keeping track of why a certain domain ended up permissioned.
in: formData
name: private_comment
type: string
produces:
- application/json
responses:
"200":
description: The newly created domain permission draft.
schema:
$ref: '#/definitions/domainPermission'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"406":
description: not acceptable
"409":
description: conflict
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Create a domain permission draft with the given parameters.
tags:
- admin
/api/v1/admin/domain_permission_drafts/{id}:
get:
operationId: domainPermissionDraftGet
parameters:
- description: ID of the domain permission draft.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Domain permission draft.
schema:
$ref: '#/definitions/domainPermission'
"401":
description: unauthorized
"403":
description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Get domain permission draft with the given ID.
tags:
- admin
/api/v1/admin/domain_permission_drafts/{id}/accept:
post:
consumes:
- multipart/form-data
- application/json
operationId: domainPermissionDraftAccept
parameters:
- description: ID of the domain permission draft.
in: path
name: id
required: true
type: string
- default: false
description: If a domain permission already exists with the same domain and permission type as the draft, overwrite the existing permission with fields from the draft.
in: formData
name: overwrite
type: boolean
produces:
- application/json
responses:
"200":
description: The newly created domain permission.
schema:
$ref: '#/definitions/domainPermission'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"406":
description: not acceptable
"409":
description: conflict
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Accept a domain permission draft, turning it into an enforced domain permission.
tags:
- admin
/api/v1/admin/domain_permission_drafts/{id}/remove:
post:
consumes:
- multipart/form-data
- application/json
operationId: domainPermissionDraftRemove
parameters:
- description: ID of the domain permission draft.
in: path
name: id
required: true
type: string
- default: false
description: When removing the domain permission draft, also create a domain ignore entry for the target domain, so that drafts will not be created for this domain in the future.
in: formData
name: ignore_target
type: boolean
produces:
- application/json
responses:
"200":
description: The removed domain permission draft.
schema:
$ref: '#/definitions/domainPermission'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"406":
description: not acceptable
"409":
description: conflict
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain.
tags:
- admin
/api/v1/admin/email/test:
post:
consumes:

View file

@ -28,37 +28,41 @@
)
const (
BasePath = "/v1/admin"
EmojiPath = BasePath + "/custom_emojis"
EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
EmojiCategoriesPath = EmojiPath + "/categories"
DomainBlocksPath = BasePath + "/domain_blocks"
DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
DomainAllowsPath = BasePath + "/domain_allows"
DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
HeaderAllowsPath = BasePath + "/header_allows"
HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
HeaderBlocksPath = BasePath + "/header_blocks"
HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
AccountsV1Path = BasePath + "/accounts"
AccountsV2Path = "/v2/admin/accounts"
AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
AccountsActionPath = AccountsPathWithID + "/action"
AccountsApprovePath = AccountsPathWithID + "/approve"
AccountsRejectPath = AccountsPathWithID + "/reject"
MediaCleanupPath = BasePath + "/media_cleanup"
MediaRefetchPath = BasePath + "/media_refetch"
ReportsPath = BasePath + "/reports"
ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
ReportsResolvePath = ReportsPathWithID + "/resolve"
EmailPath = BasePath + "/email"
EmailTestPath = EmailPath + "/test"
InstanceRulesPath = BasePath + "/instance/rules"
InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
DebugPath = BasePath + "/debug"
DebugAPUrlPath = DebugPath + "/apurl"
DebugClearCachesPath = DebugPath + "/caches/clear"
BasePath = "/v1/admin"
EmojiPath = BasePath + "/custom_emojis"
EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
EmojiCategoriesPath = EmojiPath + "/categories"
DomainBlocksPath = BasePath + "/domain_blocks"
DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
DomainAllowsPath = BasePath + "/domain_allows"
DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
DomainPermissionDraftsPath = BasePath + "/domain_permission_drafts"
DomainPermissionDraftsPathWithID = DomainPermissionDraftsPath + "/:" + apiutil.IDKey
DomainPermissionDraftAcceptPath = DomainPermissionDraftsPathWithID + "/accept"
DomainPermissionDraftRemovePath = DomainPermissionDraftsPathWithID + "/remove"
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
HeaderAllowsPath = BasePath + "/header_allows"
HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
HeaderBlocksPath = BasePath + "/header_blocks"
HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
AccountsV1Path = BasePath + "/accounts"
AccountsV2Path = "/v2/admin/accounts"
AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
AccountsActionPath = AccountsPathWithID + "/action"
AccountsApprovePath = AccountsPathWithID + "/approve"
AccountsRejectPath = AccountsPathWithID + "/reject"
MediaCleanupPath = BasePath + "/media_cleanup"
MediaRefetchPath = BasePath + "/media_refetch"
ReportsPath = BasePath + "/reports"
ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
ReportsResolvePath = ReportsPathWithID + "/resolve"
EmailPath = BasePath + "/email"
EmailTestPath = EmailPath + "/test"
InstanceRulesPath = BasePath + "/instance/rules"
InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
DebugPath = BasePath + "/debug"
DebugAPUrlPath = DebugPath + "/apurl"
DebugClearCachesPath = DebugPath + "/caches/clear"
FilterQueryKey = "filter"
MaxShortcodeDomainKey = "max_shortcode_domain"
@ -99,6 +103,13 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
attachHandler(http.MethodGet, DomainAllowsPathWithID, m.DomainAllowGETHandler)
attachHandler(http.MethodDelete, DomainAllowsPathWithID, m.DomainAllowDELETEHandler)
// domain permission draft stuff
attachHandler(http.MethodPost, DomainPermissionDraftsPath, m.DomainPermissionDraftsPOSTHandler)
attachHandler(http.MethodGet, DomainPermissionDraftsPath, m.DomainPermissionDraftsGETHandler)
attachHandler(http.MethodGet, DomainPermissionDraftsPathWithID, m.DomainPermissionDraftGETHandler)
attachHandler(http.MethodPost, DomainPermissionDraftAcceptPath, m.DomainPermissionDraftAcceptPOSTHandler)
attachHandler(http.MethodPost, DomainPermissionDraftRemovePath, m.DomainPermissionDraftRemovePOSTHandler)
// header filtering administration routes
attachHandler(http.MethodGet, HeaderAllowsPathWithID, m.HeaderFilterAllowGET)
attachHandler(http.MethodGet, HeaderBlocksPathWithID, m.HeaderFilterBlockGET)

View file

@ -0,0 +1,104 @@
// 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"
)
// DomainPermissionDraftGETHandler swagger:operation GET /api/v1/admin/domain_permission_drafts/{id} domainPermissionDraftGet
//
// Get domain permission draft with the given ID.
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// required: true
// in: path
// description: ID of the domain permission draft.
// type: string
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: Domain permission draft.
// schema:
// "$ref": "#/definitions/domainPermission"
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) DomainPermissionDraftGETHandler(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 authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
permDraft, errWithCode := m.processor.Admin().DomainPermissionDraftGet(c.Request.Context(), id)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, permDraft)
}

View file

@ -0,0 +1,134 @@
// 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"
)
// DomainPermissionDraftAcceptPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts/{id}/accept domainPermissionDraftAccept
//
// Accept a domain permission draft, turning it into an enforced domain permission.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
// - application/json
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// required: true
// in: path
// description: ID of the domain permission draft.
// type: string
// -
// name: overwrite
// in: formData
// description: >-
// If a domain permission already exists with the same
// domain and permission type as the draft, overwrite
// the existing permission with fields from the draft.
// type: boolean
// default: false
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The newly created domain permission.
// schema:
// "$ref": "#/definitions/domainPermission"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '406':
// description: not acceptable
// '409':
// description: conflict
// '500':
// description: internal server error
func (m *Module) DomainPermissionDraftAcceptPOSTHandler(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 authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
type AcceptForm struct {
Overwrite bool `json:"overwrite" form:"overwrite"`
}
form := new(AcceptForm)
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
domainPerm, _, errWithCode := m.processor.Admin().DomainPermissionDraftAccept(
c.Request.Context(),
authed.Account,
id,
form.Overwrite,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, domainPerm)
}

View file

@ -0,0 +1,176 @@
// 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/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// DomainPermissionDraftsPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts domainPermissionDraftCreate
//
// Create a domain permission draft with the given parameters.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
// - application/json
//
// produces:
// - application/json
//
// parameters:
// -
// name: domain
// in: formData
// description: Domain to create the permission draft for.
// type: string
// -
// name: permission_type
// type: string
// description: Create a draft "allow" or a draft "block".
// in: query
// -
// name: obfuscate
// in: formData
// description: >-
// Obfuscate the name of the domain when serving it publicly.
// Eg., `example.org` becomes something like `ex***e.org`.
// type: boolean
// -
// name: public_comment
// in: formData
// description: >-
// Public comment about this domain permission.
// This will be displayed alongside the domain permission if you choose to share permissions.
// type: string
// -
// name: private_comment
// in: formData
// description: >-
// Private comment about this domain permission. Will only be shown to other admins, so this
// is a useful way of internally keeping track of why a certain domain ended up permissioned.
// type: string
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The newly created domain permission draft.
// schema:
// "$ref": "#/definitions/domainPermission"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '406':
// description: not acceptable
// '409':
// description: conflict
// '500':
// description: internal server error
func (m *Module) DomainPermissionDraftsPOSTHandler(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 authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
// Parse + validate form.
form := new(apimodel.DomainPermissionRequest)
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
if form.Domain == "" {
const errText = "domain must be set"
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
var (
permType gtsmodel.DomainPermissionType
errText string
)
switch pt := form.PermissionType; pt {
case "block":
permType = gtsmodel.DomainPermissionBlock
case "allow":
permType = gtsmodel.DomainPermissionAllow
case "":
errText = "permission_type not set, must be one of block or allow"
default:
errText = fmt.Sprintf("permission_type %s not recognized, must be one of block or allow", pt)
}
if errText != "" {
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
permDraft, errWithCode := m.processor.Admin().DomainPermissionDraftCreate(
c.Request.Context(),
authed.Account,
form.Domain,
permType,
form.Obfuscate,
form.PublicComment,
form.PrivateComment,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, permDraft)
}

View file

@ -0,0 +1,134 @@
// 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"
)
// DomainPermissionDraftRemovePOSTHandler swagger:operation POST /api/v1/admin/domain_permission_drafts/{id}/remove domainPermissionDraftRemove
//
// Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
// - application/json
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// required: true
// in: path
// description: ID of the domain permission draft.
// type: string
// -
// name: ignore_target
// in: formData
// description: >-
// When removing the domain permission draft, also create a
// domain ignore entry for the target domain, so that drafts
// will not be created for this domain in the future.
// type: boolean
// default: false
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The removed domain permission draft.
// schema:
// "$ref": "#/definitions/domainPermission"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '406':
// description: not acceptable
// '409':
// description: conflict
// '500':
// description: internal server error
func (m *Module) DomainPermissionDraftRemovePOSTHandler(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 authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
type RemoveForm struct {
IgnoreTarget bool `json:"ignore_target" form:"ignore_target"`
}
form := new(RemoveForm)
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
domainPerm, errWithCode := m.processor.Admin().DomainPermissionDraftRemove(
c.Request.Context(),
authed.Account,
id,
form.IgnoreTarget,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, domainPerm)
}

View file

@ -0,0 +1,189 @@
// 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/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/paging"
)
// DomainPermissionDraftsGETHandler swagger:operation GET /api/v1/admin/domain_permission_drafts domainPermissionDraftsGet
//
// View domain permission drafts.
//
// The drafts will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
//
// The next and previous queries can be parsed from the returned Link header.
//
// Example:
//
// ```
// <https://example.org/api/v1/admin/domain_permission_drafts?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_drafts?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
// ````
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
// -
// name: subscription_id
// type: string
// description: Show only drafts created by the given subscription ID.
// in: query
// -
// name: domain
// type: string
// description: Return only drafts that target the given domain.
// in: query
// -
// name: permission_type
// type: string
// description: Filter on "block" or "allow" type drafts.
// in: query
// -
// name: max_id
// type: string
// description: >-
// Return only items *OLDER* than the given max ID (for paging downwards).
// The item with the specified ID will not be included in the response.
// in: query
// -
// name: since_id
// type: string
// description: >-
// Return only items *NEWER* than the given since ID.
// The item with the specified ID will not be included in the response.
// in: query
// -
// name: min_id
// type: string
// description: >-
// Return only items immediately *NEWER* than the given min ID (for paging upwards).
// The item with the specified ID will not be included in the response.
// in: query
// -
// name: limit
// type: integer
// description: Number of items to return.
// default: 20
// minimum: 1
// maximum: 100
// in: query
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: Domain permission drafts.
// schema:
// type: array
// items:
// "$ref": "#/definitions/domainPermission"
// headers:
// Link:
// type: string
// description: Links to the next and previous queries.
// '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) DomainPermissionDraftsGETHandler(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 authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
permType := c.Query(apiutil.DomainPermissionPermTypeKey)
switch permType {
case "", "block", "allow":
// No problem.
default:
// Invalid.
text := fmt.Sprintf(
"permission_type %s not recognized, valid values are empty string, block, or allow",
permType,
)
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
resp, errWithCode := m.processor.Admin().DomainPermissionDraftsGet(
c.Request.Context(),
c.Query(apiutil.DomainPermissionSubscriptionIDKey),
c.Query(apiutil.DomainPermissionDomainKey),
gtsmodel.NewDomainPermissionType(permType),
page,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
apiutil.JSON(c, http.StatusOK, resp.Items)
}

View file

@ -61,6 +61,53 @@ type DomainPermission struct {
// Time at which the permission entry was created (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
CreatedAt string `json:"created_at,omitempty"`
// Permission type of this entry (block, allow).
// Only set for domain permission drafts.
PermissionType string `json:"permission_type,omitempty"`
}
// DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
//
// swagger:model domainPermissionSubscription
type DomainPermissionSubscription struct {
// The ID of the domain permission subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
ID string `json:"id"`
// The type of domain permission subscription (allow, block).
// example: block
PermissionType string `json:"permission_type"`
// If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. If false, domain permissions from this subscription will come into force immediately.
// example: true
AsDraft bool `json:"as_draft"`
// ID of the account that created this subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
CreatedByAccountID string `json:"created_by_account_id"`
// MIME content type to expect at URI.
// example: text/csv
ContentType string `json:"content_type"`
// URI to call in order to fetch the permissions list.
// example: https://www.example.org/blocklists/list1.csv
URI string `json:"uri"`
// (Optional) username to set for basic auth when doing a fetch of URI.
// example: admin123
FetchUsername string `json:"fetch_username"`
// (Optional) password to set for basic auth when doing a fetch of URI.
// example: admin123
FetchPassword string `json:"fetch_password"`
// Time at which the most recent fetch was attempted (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
// readonly: true
FetchedAt string `json:"fetched_at"`
// If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
// example: Oopsie doopsie, we made a fucky wucky.
// readonly: true
Error string `json:"error"`
// Count of domain permission entries discovered at URI.
// example: 53
// readonly: true
Count uint64 `json:"count"`
}
// DomainPermissionRequest is the form submitted as a POST to create a new domain permission entry (allow/block).
@ -69,22 +116,24 @@ type DomainPermission struct {
type DomainPermissionRequest struct {
// A list of domains for which this permission request should apply.
// Only used if import=true is specified.
Domains *multipart.FileHeader `form:"domains" json:"domains" xml:"domains"`
Domains *multipart.FileHeader `form:"domains" json:"domains"`
// A single domain for which this permission request should apply.
// Only used if import=true is NOT specified or if import=false.
// example: example.org
Domain string `form:"domain" json:"domain" xml:"domain"`
Domain string `form:"domain" json:"domain"`
// Obfuscate the domain name when displaying this permission entry publicly.
// Ie., instead of 'example.org' show something like 'e**mpl*.or*'.
// example: false
Obfuscate bool `form:"obfuscate" json:"obfuscate" xml:"obfuscate"`
Obfuscate bool `form:"obfuscate" json:"obfuscate"`
// Private comment for other admins on why this permission entry was created.
// example: don't like 'em!!!!
PrivateComment string `form:"private_comment" json:"private_comment" xml:"private_comment"`
PrivateComment string `form:"private_comment" json:"private_comment"`
// Public comment on why this permission entry was created.
// Will be visible to requesters at /api/v1/instance/peers if this endpoint is exposed.
// example: foss dorks 😫
PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"`
PublicComment string `form:"public_comment" json:"public_comment"`
// Permission type to create (only applies to domain permission drafts, not explicit blocks and allows).
PermissionType string `form:"permission_type" json:"permission_type"`
}
// DomainKeysExpireRequest is the form submitted as a POST to /api/v1/admin/domain_keys_expire to expire a domain's public keys.
@ -92,5 +141,5 @@ type DomainPermissionRequest struct {
// swagger:parameters domainKeysExpire
type DomainKeysExpireRequest struct {
// hostname/domain to expire keys for.
Domain string `form:"domain" json:"domain" xml:"domain"`
Domain string `form:"domain" json:"domain"`
}

View file

@ -69,8 +69,11 @@
/* Domain permission keys */
DomainPermissionExportKey = "export"
DomainPermissionImportKey = "import"
DomainPermissionExportKey = "export"
DomainPermissionImportKey = "import"
DomainPermissionSubscriptionIDKey = "subscription_id"
DomainPermissionPermTypeKey = "permission_type"
DomainPermissionDomainKey = "domain"
/* Admin query keys */

View file

@ -74,6 +74,9 @@ func (c *Caches) Init() {
c.initConversationLastStatusIDs()
c.initDomainAllow()
c.initDomainBlock()
c.initDomainPermissionDraft()
c.initDomainPermissionSubscription()
c.initDomainPermissionIgnore()
c.initEmoji()
c.initEmojiCategory()
c.initFilter()

76
internal/cache/db.go vendored
View file

@ -67,6 +67,15 @@ type DBCaches struct {
// DomainBlock provides access to the domain block database cache.
DomainBlock *domain.Cache
// DomainPermissionDraft provides access to the domain permission draft database cache.
DomainPermissionDraft StructCache[*gtsmodel.DomainPermissionDraft]
// DomainPermissionSubscription provides access to the domain permission subscription database cache.
DomainPermissionSubscription StructCache[*gtsmodel.DomainPermissionSubscription]
// DomainPermissionIgnore provides access to the domain permission ignore database cache.
DomainPermissionIgnore *domain.Cache
// Emoji provides access to the gtsmodel Emoji database cache.
Emoji StructCache[*gtsmodel.Emoji]
@ -548,6 +557,73 @@ func (c *Caches) initDomainBlock() {
c.DB.DomainBlock = new(domain.Cache)
}
func (c *Caches) initDomainPermissionDraft() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
sizeofDomainPermissionDraft(), // model in-mem size.
config.GetCacheDomainPermissionDraftMemRation(),
)
log.Infof(nil, "cache size = %d", cap)
copyF := func(d1 *gtsmodel.DomainPermissionDraft) *gtsmodel.DomainPermissionDraft {
d2 := new(gtsmodel.DomainPermissionDraft)
*d2 = *d1
// Don't include ptr fields that
// will be populated separately.
d2.CreatedByAccount = nil
return d2
}
c.DB.DomainPermissionDraft.Init(structr.CacheConfig[*gtsmodel.DomainPermissionDraft]{
Indices: []structr.IndexConfig{
{Fields: "ID"},
{Fields: "Domain", Multiple: true},
{Fields: "SubscriptionID", Multiple: true},
},
MaxSize: cap,
IgnoreErr: ignoreErrors,
Copy: copyF,
})
}
func (c *Caches) initDomainPermissionSubscription() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
sizeofDomainPermissionSubscription(), // model in-mem size.
config.GetCacheDomainPermissionSubscriptionMemRation(),
)
log.Infof(nil, "cache size = %d", cap)
copyF := func(d1 *gtsmodel.DomainPermissionSubscription) *gtsmodel.DomainPermissionSubscription {
d2 := new(gtsmodel.DomainPermissionSubscription)
*d2 = *d1
// Don't include ptr fields that
// will be populated separately.
d2.CreatedByAccount = nil
return d2
}
c.DB.DomainPermissionSubscription.Init(structr.CacheConfig[*gtsmodel.DomainPermissionSubscription]{
Indices: []structr.IndexConfig{
{Fields: "ID"},
{Fields: "URI"},
},
MaxSize: cap,
IgnoreErr: ignoreErrors,
Copy: copyF,
})
}
func (c *Caches) initDomainPermissionIgnore() {
c.DB.DomainPermissionIgnore = new(domain.Cache)
}
func (c *Caches) initEmoji() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(

View file

@ -342,6 +342,35 @@ func sizeofConversation() uintptr {
}))
}
func sizeofDomainPermissionDraft() uintptr {
return uintptr(size.Of(&gtsmodel.DomainPermissionDraft{
ID: exampleID,
CreatedAt: exampleTime,
UpdatedAt: exampleTime,
PermissionType: gtsmodel.DomainPermissionBlock,
Domain: "example.org",
CreatedByAccountID: exampleID,
PrivateComment: exampleTextSmall,
PublicComment: exampleTextSmall,
Obfuscate: util.Ptr(false),
SubscriptionID: exampleID,
}))
}
func sizeofDomainPermissionSubscription() uintptr {
return uintptr(size.Of(&gtsmodel.DomainPermissionSubscription{
ID: exampleID,
CreatedAt: exampleTime,
PermissionType: gtsmodel.DomainPermissionBlock,
CreatedByAccountID: exampleID,
URI: exampleURI,
FetchUsername: "username",
FetchPassword: "password",
FetchedAt: exampleTime,
AsDraft: util.Ptr(true),
}))
}
func sizeofEmoji() uintptr {
return uintptr(size.Of(&gtsmodel.Emoji{
ID: exampleID,

View file

@ -194,58 +194,60 @@ type HTTPClientConfiguration struct {
}
type CacheConfiguration struct {
MemoryTarget bytesize.Size `name:"memory-target"`
AccountMemRatio float64 `name:"account-mem-ratio"`
AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"`
AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"`
ApplicationMemRatio float64 `name:"application-mem-ratio"`
BlockMemRatio float64 `name:"block-mem-ratio"`
BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"`
BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"`
ClientMemRatio float64 `name:"client-mem-ratio"`
ConversationMemRatio float64 `name:"conversation-mem-ratio"`
ConversationLastStatusIDsMemRatio float64 `name:"conversation-last-status-ids-mem-ratio"`
EmojiMemRatio float64 `name:"emoji-mem-ratio"`
EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"`
FilterMemRatio float64 `name:"filter-mem-ratio"`
FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"`
FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"`
FollowMemRatio float64 `name:"follow-mem-ratio"`
FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"`
FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"`
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
FollowingTagIDsMemRatio float64 `name:"following-tag-ids-mem-ratio"`
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
InstanceMemRatio float64 `name:"instance-mem-ratio"`
InteractionRequestMemRatio float64 `name:"interaction-request-mem-ratio"`
ListMemRatio float64 `name:"list-mem-ratio"`
ListIDsMemRatio float64 `name:"list-ids-mem-ratio"`
ListedIDsMemRatio float64 `name:"listed-ids-mem-ratio"`
MarkerMemRatio float64 `name:"marker-mem-ratio"`
MediaMemRatio float64 `name:"media-mem-ratio"`
MentionMemRatio float64 `name:"mention-mem-ratio"`
MoveMemRatio float64 `name:"move-mem-ratio"`
NotificationMemRatio float64 `name:"notification-mem-ratio"`
PollMemRatio float64 `name:"poll-mem-ratio"`
PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"`
PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"`
ReportMemRatio float64 `name:"report-mem-ratio"`
SinBinStatusMemRatio float64 `name:"sin-bin-status-mem-ratio"`
StatusMemRatio float64 `name:"status-mem-ratio"`
StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"`
StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"`
StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
TagMemRatio float64 `name:"tag-mem-ratio"`
ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"`
TokenMemRatio float64 `name:"token-mem-ratio"`
TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
UserMemRatio float64 `name:"user-mem-ratio"`
UserMuteMemRatio float64 `name:"user-mute-mem-ratio"`
UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"`
WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
VisibilityMemRatio float64 `name:"visibility-mem-ratio"`
MemoryTarget bytesize.Size `name:"memory-target"`
AccountMemRatio float64 `name:"account-mem-ratio"`
AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"`
AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"`
ApplicationMemRatio float64 `name:"application-mem-ratio"`
BlockMemRatio float64 `name:"block-mem-ratio"`
BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"`
BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"`
ClientMemRatio float64 `name:"client-mem-ratio"`
ConversationMemRatio float64 `name:"conversation-mem-ratio"`
ConversationLastStatusIDsMemRatio float64 `name:"conversation-last-status-ids-mem-ratio"`
DomainPermissionDraftMemRation float64 `name:"domain-permission-draft-mem-ratio"`
DomainPermissionSubscriptionMemRation float64 `name:"domain-permission-subscription-mem-ratio"`
EmojiMemRatio float64 `name:"emoji-mem-ratio"`
EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"`
FilterMemRatio float64 `name:"filter-mem-ratio"`
FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"`
FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"`
FollowMemRatio float64 `name:"follow-mem-ratio"`
FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"`
FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"`
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
FollowingTagIDsMemRatio float64 `name:"following-tag-ids-mem-ratio"`
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
InstanceMemRatio float64 `name:"instance-mem-ratio"`
InteractionRequestMemRatio float64 `name:"interaction-request-mem-ratio"`
ListMemRatio float64 `name:"list-mem-ratio"`
ListIDsMemRatio float64 `name:"list-ids-mem-ratio"`
ListedIDsMemRatio float64 `name:"listed-ids-mem-ratio"`
MarkerMemRatio float64 `name:"marker-mem-ratio"`
MediaMemRatio float64 `name:"media-mem-ratio"`
MentionMemRatio float64 `name:"mention-mem-ratio"`
MoveMemRatio float64 `name:"move-mem-ratio"`
NotificationMemRatio float64 `name:"notification-mem-ratio"`
PollMemRatio float64 `name:"poll-mem-ratio"`
PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"`
PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"`
ReportMemRatio float64 `name:"report-mem-ratio"`
SinBinStatusMemRatio float64 `name:"sin-bin-status-mem-ratio"`
StatusMemRatio float64 `name:"status-mem-ratio"`
StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"`
StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"`
StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
TagMemRatio float64 `name:"tag-mem-ratio"`
ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"`
TokenMemRatio float64 `name:"token-mem-ratio"`
TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
UserMemRatio float64 `name:"user-mem-ratio"`
UserMuteMemRatio float64 `name:"user-mute-mem-ratio"`
UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"`
WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
VisibilityMemRatio float64 `name:"visibility-mem-ratio"`
}
// MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML).

View file

@ -158,57 +158,59 @@
// when TODO items in the size.go source
// file have been addressed, these should
// be able to make some more sense :D
AccountMemRatio: 5,
AccountNoteMemRatio: 1,
AccountSettingsMemRatio: 0.1,
AccountStatsMemRatio: 2,
ApplicationMemRatio: 0.1,
BlockMemRatio: 2,
BlockIDsMemRatio: 3,
BoostOfIDsMemRatio: 3,
ClientMemRatio: 0.1,
ConversationMemRatio: 1,
ConversationLastStatusIDsMemRatio: 2,
EmojiMemRatio: 3,
EmojiCategoryMemRatio: 0.1,
FilterMemRatio: 0.5,
FilterKeywordMemRatio: 0.5,
FilterStatusMemRatio: 0.5,
FollowMemRatio: 2,
FollowIDsMemRatio: 4,
FollowRequestMemRatio: 2,
FollowRequestIDsMemRatio: 2,
FollowingTagIDsMemRatio: 2,
InReplyToIDsMemRatio: 3,
InstanceMemRatio: 1,
InteractionRequestMemRatio: 1,
ListMemRatio: 1,
ListIDsMemRatio: 2,
ListedIDsMemRatio: 2,
MarkerMemRatio: 0.5,
MediaMemRatio: 4,
MentionMemRatio: 2,
MoveMemRatio: 0.1,
NotificationMemRatio: 2,
PollMemRatio: 1,
PollVoteMemRatio: 2,
PollVoteIDsMemRatio: 2,
ReportMemRatio: 1,
SinBinStatusMemRatio: 0.5,
StatusMemRatio: 5,
StatusBookmarkMemRatio: 0.5,
StatusBookmarkIDsMemRatio: 2,
StatusFaveMemRatio: 2,
StatusFaveIDsMemRatio: 3,
TagMemRatio: 2,
ThreadMuteMemRatio: 0.2,
TokenMemRatio: 0.75,
TombstoneMemRatio: 0.5,
UserMemRatio: 0.25,
UserMuteMemRatio: 2,
UserMuteIDsMemRatio: 3,
WebfingerMemRatio: 0.1,
VisibilityMemRatio: 2,
AccountMemRatio: 5,
AccountNoteMemRatio: 1,
AccountSettingsMemRatio: 0.1,
AccountStatsMemRatio: 2,
ApplicationMemRatio: 0.1,
BlockMemRatio: 2,
BlockIDsMemRatio: 3,
BoostOfIDsMemRatio: 3,
ClientMemRatio: 0.1,
ConversationMemRatio: 1,
ConversationLastStatusIDsMemRatio: 2,
DomainPermissionDraftMemRation: 0.5,
DomainPermissionSubscriptionMemRation: 0.5,
EmojiMemRatio: 3,
EmojiCategoryMemRatio: 0.1,
FilterMemRatio: 0.5,
FilterKeywordMemRatio: 0.5,
FilterStatusMemRatio: 0.5,
FollowMemRatio: 2,
FollowIDsMemRatio: 4,
FollowRequestMemRatio: 2,
FollowRequestIDsMemRatio: 2,
FollowingTagIDsMemRatio: 2,
InReplyToIDsMemRatio: 3,
InstanceMemRatio: 1,
InteractionRequestMemRatio: 1,
ListMemRatio: 1,
ListIDsMemRatio: 2,
ListedIDsMemRatio: 2,
MarkerMemRatio: 0.5,
MediaMemRatio: 4,
MentionMemRatio: 2,
MoveMemRatio: 0.1,
NotificationMemRatio: 2,
PollMemRatio: 1,
PollVoteMemRatio: 2,
PollVoteIDsMemRatio: 2,
ReportMemRatio: 1,
SinBinStatusMemRatio: 0.5,
StatusMemRatio: 5,
StatusBookmarkMemRatio: 0.5,
StatusBookmarkIDsMemRatio: 2,
StatusFaveMemRatio: 2,
StatusFaveIDsMemRatio: 3,
TagMemRatio: 2,
ThreadMuteMemRatio: 0.2,
TokenMemRatio: 0.75,
TombstoneMemRatio: 0.5,
UserMemRatio: 0.25,
UserMuteMemRatio: 2,
UserMuteIDsMemRatio: 3,
WebfingerMemRatio: 0.1,
VisibilityMemRatio: 2,
},
HTTPClient: HTTPClientConfiguration{

View file

@ -3106,6 +3106,68 @@ func SetCacheConversationLastStatusIDsMemRatio(v float64) {
global.SetCacheConversationLastStatusIDsMemRatio(v)
}
// GetCacheDomainPermissionDraftMemRation safely fetches the Configuration value for state's 'Cache.DomainPermissionDraftMemRation' field
func (st *ConfigState) GetCacheDomainPermissionDraftMemRation() (v float64) {
st.mutex.RLock()
v = st.config.Cache.DomainPermissionDraftMemRation
st.mutex.RUnlock()
return
}
// SetCacheDomainPermissionDraftMemRation safely sets the Configuration value for state's 'Cache.DomainPermissionDraftMemRation' field
func (st *ConfigState) SetCacheDomainPermissionDraftMemRation(v float64) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.Cache.DomainPermissionDraftMemRation = v
st.reloadToViper()
}
// CacheDomainPermissionDraftMemRationFlag returns the flag name for the 'Cache.DomainPermissionDraftMemRation' field
func CacheDomainPermissionDraftMemRationFlag() string {
return "cache-domain-permission-draft-mem-ratio"
}
// GetCacheDomainPermissionDraftMemRation safely fetches the value for global configuration 'Cache.DomainPermissionDraftMemRation' field
func GetCacheDomainPermissionDraftMemRation() float64 {
return global.GetCacheDomainPermissionDraftMemRation()
}
// SetCacheDomainPermissionDraftMemRation safely sets the value for global configuration 'Cache.DomainPermissionDraftMemRation' field
func SetCacheDomainPermissionDraftMemRation(v float64) {
global.SetCacheDomainPermissionDraftMemRation(v)
}
// GetCacheDomainPermissionSubscriptionMemRation safely fetches the Configuration value for state's 'Cache.DomainPermissionSubscriptionMemRation' field
func (st *ConfigState) GetCacheDomainPermissionSubscriptionMemRation() (v float64) {
st.mutex.RLock()
v = st.config.Cache.DomainPermissionSubscriptionMemRation
st.mutex.RUnlock()
return
}
// SetCacheDomainPermissionSubscriptionMemRation safely sets the Configuration value for state's 'Cache.DomainPermissionSubscriptionMemRation' field
func (st *ConfigState) SetCacheDomainPermissionSubscriptionMemRation(v float64) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.Cache.DomainPermissionSubscriptionMemRation = v
st.reloadToViper()
}
// CacheDomainPermissionSubscriptionMemRationFlag returns the flag name for the 'Cache.DomainPermissionSubscriptionMemRation' field
func CacheDomainPermissionSubscriptionMemRationFlag() string {
return "cache-domain-permission-subscription-mem-ratio"
}
// GetCacheDomainPermissionSubscriptionMemRation safely fetches the value for global configuration 'Cache.DomainPermissionSubscriptionMemRation' field
func GetCacheDomainPermissionSubscriptionMemRation() float64 {
return global.GetCacheDomainPermissionSubscriptionMemRation()
}
// SetCacheDomainPermissionSubscriptionMemRation safely sets the value for global configuration 'Cache.DomainPermissionSubscriptionMemRation' field
func SetCacheDomainPermissionSubscriptionMemRation(v float64) {
global.SetCacheDomainPermissionSubscriptionMemRation(v)
}
// GetCacheEmojiMemRatio safely fetches the Configuration value for state's 'Cache.EmojiMemRatio' field
func (st *ConfigState) GetCacheEmojiMemRatio() (v float64) {
st.mutex.RLock()

View file

@ -20,6 +20,7 @@
import (
"context"
"net/url"
"time"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -110,6 +111,36 @@ func (d *domainDB) GetDomainAllowByID(ctx context.Context, id string) (*gtsmodel
return &allow, nil
}
func (d *domainDB) UpdateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow, columns ...string) error {
// Normalize the domain as punycode
var err error
allow.Domain, err = util.Punify(allow.Domain)
if err != nil {
return err
}
// Ensure updated_at is set.
allow.UpdatedAt = time.Now()
if len(columns) != 0 {
columns = append(columns, "updated_at")
}
// Attempt to update domain allow.
if _, err := d.db.
NewUpdate().
Model(allow).
Column(columns...).
Where("? = ?", bun.Ident("domain_allow.id"), allow.ID).
Exec(ctx); err != nil {
return err
}
// Clear the domain allow cache (for later reload)
d.state.Caches.DB.DomainAllow.Clear()
return nil
}
func (d *domainDB) DeleteDomainAllow(ctx context.Context, domain string) error {
// Normalize the domain as punycode
domain, err := util.Punify(domain)
@ -206,6 +237,36 @@ func (d *domainDB) GetDomainBlockByID(ctx context.Context, id string) (*gtsmodel
return &block, nil
}
func (d *domainDB) UpdateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock, columns ...string) error {
// Normalize the domain as punycode
var err error
block.Domain, err = util.Punify(block.Domain)
if err != nil {
return err
}
// Ensure updated_at is set.
block.UpdatedAt = time.Now()
if len(columns) != 0 {
columns = append(columns, "updated_at")
}
// Attempt to update domain block.
if _, err := d.db.
NewUpdate().
Model(block).
Column(columns...).
Where("? = ?", bun.Ident("domain_block.id"), block.ID).
Exec(ctx); err != nil {
return err
}
// Clear the domain block cache (for later reload)
d.state.Caches.DB.DomainBlock.Clear()
return nil
}
func (d *domainDB) DeleteDomainBlock(ctx context.Context, domain string) error {
// Normalize the domain as punycode
domain, err := util.Punify(domain)

View file

@ -0,0 +1,285 @@
// 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"
"slices"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
)
func (d *domainDB) getDomainPermissionDraft(
ctx context.Context,
lookup string,
dbQuery func(*gtsmodel.DomainPermissionDraft) error,
keyParts ...any,
) (*gtsmodel.DomainPermissionDraft, error) {
// Fetch perm draft from database cache with loader callback.
permDraft, err := d.state.Caches.DB.DomainPermissionDraft.LoadOne(
lookup,
// Only called if not cached.
func() (*gtsmodel.DomainPermissionDraft, error) {
var permDraft gtsmodel.DomainPermissionDraft
if err := dbQuery(&permDraft); err != nil {
return nil, err
}
return &permDraft, nil
},
keyParts...,
)
if err != nil {
return nil, err
}
if gtscontext.Barebones(ctx) {
// No need to fully populate.
return permDraft, nil
}
if permDraft.CreatedByAccount == nil {
// Not set, fetch from database.
permDraft.CreatedByAccount, err = d.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
permDraft.CreatedByAccountID,
)
if err != nil {
return nil, gtserror.Newf("error populating created by account: %w", err)
}
}
return permDraft, nil
}
func (d *domainDB) GetDomainPermissionDraftByID(
ctx context.Context,
id string,
) (*gtsmodel.DomainPermissionDraft, error) {
return d.getDomainPermissionDraft(
ctx,
"ID",
func(permDraft *gtsmodel.DomainPermissionDraft) error {
return d.db.
NewSelect().
Model(permDraft).
Where("? = ?", bun.Ident("domain_permission_draft.id"), id).
Scan(ctx)
},
id,
)
}
func (d *domainDB) GetDomainPermissionDrafts(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
permSubID string,
domain string,
page *paging.Page,
) (
[]*gtsmodel.DomainPermissionDraft,
error,
) {
var (
// Get paging params.
minID = page.GetMin()
maxID = page.GetMax()
limit = page.GetLimit()
order = page.GetOrder()
// Make educated guess for slice size
permDraftIDs = make([]string, 0, limit)
)
q := d.db.
NewSelect().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_drafts"),
bun.Ident("domain_permission_draft"),
).
// Select only IDs from table
Column("domain_permission_draft.id")
// Return only items with id
// lower than provided maxID.
if maxID != "" {
q = q.Where(
"? < ?",
bun.Ident("domain_permission_draft.id"),
maxID,
)
}
// Return only items with id
// greater than provided minID.
if minID != "" {
q = q.Where(
"? > ?",
bun.Ident("domain_permission_draft.id"),
minID,
)
}
// Return only items with
// given permission type.
if permType != gtsmodel.DomainPermissionUnknown {
q = q.Where(
"? = ?",
bun.Ident("domain_permission_draft.permission_type"),
permType,
)
}
// Return only items with
// given subscription ID.
if permSubID != "" {
q = q.Where(
"? = ?",
bun.Ident("domain_permission_draft.subscription_id"),
permSubID,
)
}
// Return only items
// with given domain.
if domain != "" {
var err error
// Normalize domain as punycode.
domain, err = util.Punify(domain)
if err != nil {
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
}
q = q.Where(
"? = ?",
bun.Ident("domain_permission_draft.domain"),
domain,
)
}
if limit > 0 {
// Limit amount of
// items returned.
q = q.Limit(limit)
}
if order == paging.OrderAscending {
// Page up.
q = q.OrderExpr(
"? ASC",
bun.Ident("domain_permission_draft.id"),
)
} else {
// Page down.
q = q.OrderExpr(
"? DESC",
bun.Ident("domain_permission_draft.id"),
)
}
if err := q.Scan(ctx, &permDraftIDs); err != nil {
return nil, err
}
// Catch case of no items early
if len(permDraftIDs) == 0 {
return nil, db.ErrNoEntries
}
// If we're paging up, we still want items
// to be sorted by ID desc, so reverse slice.
if order == paging.OrderAscending {
slices.Reverse(permDraftIDs)
}
// Allocate return slice (will be at most len permDraftIDs)
permDrafts := make([]*gtsmodel.DomainPermissionDraft, 0, len(permDraftIDs))
for _, id := range permDraftIDs {
permDraft, err := d.GetDomainPermissionDraftByID(ctx, id)
if err != nil {
log.Errorf(ctx, "error getting domain permission draft %q: %v", id, err)
continue
}
// Append to return slice
permDrafts = append(permDrafts, permDraft)
}
return permDrafts, nil
}
func (d *domainDB) PutDomainPermissionDraft(
ctx context.Context,
permDraft *gtsmodel.DomainPermissionDraft,
) error {
var err error
// Normalize the domain as punycode
permDraft.Domain, err = util.Punify(permDraft.Domain)
if err != nil {
return gtserror.Newf("error punifying domain %s: %w", permDraft.Domain, err)
}
return d.state.Caches.DB.DomainPermissionDraft.Store(
permDraft,
func() error {
_, err := d.db.
NewInsert().
Model(permDraft).
Exec(ctx)
return err
},
)
}
func (d *domainDB) DeleteDomainPermissionDraft(
ctx context.Context,
id string,
) error {
// Delete the permDraft from DB.
q := d.db.NewDelete().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_drafts"),
bun.Ident("domain_permission_draft"),
).
Where(
"? = ?",
bun.Ident("domain_permission_draft.id"),
id,
)
_, err := q.Exec(ctx)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
// Invalidate any cached model by ID.
d.state.Caches.DB.DomainPermissionDraft.Invalidate("ID", id)
return nil
}

View file

@ -0,0 +1,275 @@
// 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"
"slices"
"github.com/miekg/dns"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
)
func (d *domainDB) PutDomainPermissionIgnore(
ctx context.Context,
ignore *gtsmodel.DomainPermissionIgnore,
) error {
// Normalize the domain as punycode
var err error
ignore.Domain, err = util.Punify(ignore.Domain)
if err != nil {
return err
}
// Attempt to store domain perm ignore in DB
if _, err := d.db.NewInsert().
Model(ignore).
Exec(ctx); err != nil {
return err
}
// Clear the domain perm ignore cache (for later reload)
d.state.Caches.DB.DomainPermissionIgnore.Clear()
return nil
}
func (d *domainDB) IsDomainPermissionIgnored(ctx context.Context, domain string) (bool, error) {
// Normalize the domain as punycode
domain, err := util.Punify(domain)
if err != nil {
return false, err
}
// Check if our host and given domain are equal
// or part of the same second-level domain; we
// always ignore such perms as creating blocks
// or allows in such cases may break things.
if dns.CompareDomainName(domain, config.GetHost()) >= 2 {
return true, nil
}
// Func to scan list of all
// ignored domain perms from DB.
loadF := func() ([]string, error) {
var domains []string
if err := d.db.
NewSelect().
Table("domain_ignores").
Column("domain").
Scan(ctx, &domains); err != nil {
return nil, err
}
return domains, nil
}
// Check the cache for a domain perm ignore,
// hydrating the cache with loadF if necessary.
return d.state.Caches.DB.DomainPermissionIgnore.Matches(domain, loadF)
}
func (d *domainDB) GetDomainPermissionIgnoreByID(
ctx context.Context,
id string,
) (*gtsmodel.DomainPermissionIgnore, error) {
ignore := new(gtsmodel.DomainPermissionIgnore)
q := d.db.
NewSelect().
Model(ignore).
Where("? = ?", bun.Ident("domain_permission_ignore.id"), id)
if err := q.Scan(ctx); err != nil {
return nil, err
}
if gtscontext.Barebones(ctx) {
// No need to fully populate.
return ignore, nil
}
if ignore.CreatedByAccount == nil {
// Not set, fetch from database.
var err error
ignore.CreatedByAccount, err = d.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
ignore.CreatedByAccountID,
)
if err != nil {
return nil, gtserror.Newf("error populating created by account: %w", err)
}
}
return ignore, nil
}
func (d *domainDB) GetDomainPermissionIgnores(
ctx context.Context,
domain string,
page *paging.Page,
) (
[]*gtsmodel.DomainPermissionIgnore,
error,
) {
var (
// Get paging params.
minID = page.GetMin()
maxID = page.GetMax()
limit = page.GetLimit()
order = page.GetOrder()
// Make educated guess for slice size
ignoreIDs = make([]string, 0, limit)
)
q := d.db.
NewSelect().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_ignores"),
bun.Ident("domain_permission_ignore"),
).
// Select only IDs from table
Column("domain_permission_ignore.id")
// Return only items with id
// lower than provided maxID.
if maxID != "" {
q = q.Where(
"? < ?",
bun.Ident("domain_permission_ignore.id"),
maxID,
)
}
// Return only items with id
// greater than provided minID.
if minID != "" {
q = q.Where(
"? > ?",
bun.Ident("domain_permission_ignore.id"),
minID,
)
}
// Return only items
// with given domain.
if domain != "" {
var err error
// Normalize domain as punycode.
domain, err = util.Punify(domain)
if err != nil {
return nil, gtserror.Newf("error punifying domain %s: %w", domain, err)
}
q = q.Where(
"? = ?",
bun.Ident("domain_permission_ignore.domain"),
domain,
)
}
if limit > 0 {
// Limit amount of
// items returned.
q = q.Limit(limit)
}
if order == paging.OrderAscending {
// Page up.
q = q.OrderExpr(
"? ASC",
bun.Ident("domain_permission_ignore.id"),
)
} else {
// Page down.
q = q.OrderExpr(
"? DESC",
bun.Ident("domain_permission_ignore.id"),
)
}
if err := q.Scan(ctx, &ignoreIDs); err != nil {
return nil, err
}
// Catch case of no items early
if len(ignoreIDs) == 0 {
return nil, db.ErrNoEntries
}
// If we're paging up, we still want items
// to be sorted by ID desc, so reverse slice.
if order == paging.OrderAscending {
slices.Reverse(ignoreIDs)
}
// Allocate return slice (will be at most len permSubIDs).
ignores := make([]*gtsmodel.DomainPermissionIgnore, 0, len(ignoreIDs))
for _, id := range ignoreIDs {
ignore, err := d.GetDomainPermissionIgnoreByID(ctx, id)
if err != nil {
log.Errorf(ctx, "error getting domain permission ignore %q: %v", id, err)
continue
}
// Append to return slice
ignores = append(ignores, ignore)
}
return ignores, nil
}
func (d *domainDB) DeleteDomainPermissionIgnore(
ctx context.Context,
id string,
) error {
// Delete the permSub from DB.
q := d.db.NewDelete().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_ignores"),
bun.Ident("domain_permission_ignore"),
).
Where(
"? = ?",
bun.Ident("domain_permission_ignore.id"),
id,
)
_, err := q.Exec(ctx)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
// Clear the domain perm ignore cache (for later reload)
d.state.Caches.DB.DomainPermissionIgnore.Clear()
return nil
}

View file

@ -0,0 +1,246 @@
// 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"
"slices"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/uptrace/bun"
)
func (d *domainDB) getDomainPermissionSubscription(
ctx context.Context,
lookup string,
dbQuery func(*gtsmodel.DomainPermissionSubscription) error,
keyParts ...any,
) (*gtsmodel.DomainPermissionSubscription, error) {
// Fetch perm subscription from database cache with loader callback.
permSub, err := d.state.Caches.DB.DomainPermissionSubscription.LoadOne(
lookup,
// Only called if not cached.
func() (*gtsmodel.DomainPermissionSubscription, error) {
var permSub gtsmodel.DomainPermissionSubscription
if err := dbQuery(&permSub); err != nil {
return nil, err
}
return &permSub, nil
},
keyParts...,
)
if err != nil {
return nil, err
}
if gtscontext.Barebones(ctx) {
// No need to fully populate.
return permSub, nil
}
if permSub.CreatedByAccount == nil {
// Not set, fetch from database.
permSub.CreatedByAccount, err = d.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
permSub.CreatedByAccountID,
)
if err != nil {
return nil, gtserror.Newf("error populating created by account: %w", err)
}
}
return permSub, nil
}
func (d *domainDB) GetDomainPermissionSubscriptionByID(
ctx context.Context,
id string,
) (*gtsmodel.DomainPermissionSubscription, error) {
return d.getDomainPermissionSubscription(
ctx,
"ID",
func(permSub *gtsmodel.DomainPermissionSubscription) error {
return d.db.
NewSelect().
Model(permSub).
Where("? = ?", bun.Ident("domain_permission_subscription.id"), id).
Scan(ctx)
},
id,
)
}
func (d *domainDB) GetDomainPermissionSubscriptions(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
page *paging.Page,
) (
[]*gtsmodel.DomainPermissionSubscription,
error,
) {
var (
// Get paging params.
minID = page.GetMin()
maxID = page.GetMax()
limit = page.GetLimit()
order = page.GetOrder()
// Make educated guess for slice size
permSubIDs = make([]string, 0, limit)
)
q := d.db.
NewSelect().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_subscriptions"),
bun.Ident("domain_permission_subscription"),
).
// Select only IDs from table
Column("domain_permission_subscription.id")
// Return only items with id
// lower than provided maxID.
if maxID != "" {
q = q.Where(
"? < ?",
bun.Ident("domain_permission_subscription.id"),
maxID,
)
}
// Return only items with id
// greater than provided minID.
if minID != "" {
q = q.Where(
"? > ?",
bun.Ident("domain_permission_subscription.id"),
minID,
)
}
// Return only items with
// given permission type.
if permType != gtsmodel.DomainPermissionUnknown {
q = q.Where(
"? = ?",
bun.Ident("domain_permission_subscription.permission_type"),
permType,
)
}
if limit > 0 {
// Limit amount of
// items returned.
q = q.Limit(limit)
}
if order == paging.OrderAscending {
// Page up.
q = q.OrderExpr(
"? ASC",
bun.Ident("domain_permission_subscription.id"),
)
} else {
// Page down.
q = q.OrderExpr(
"? DESC",
bun.Ident("domain_permission_subscription.id"),
)
}
if err := q.Scan(ctx, &permSubIDs); err != nil {
return nil, err
}
// Catch case of no items early
if len(permSubIDs) == 0 {
return nil, db.ErrNoEntries
}
// If we're paging up, we still want items
// to be sorted by ID desc, so reverse slice.
if order == paging.OrderAscending {
slices.Reverse(permSubIDs)
}
// Allocate return slice (will be at most len permSubIDs).
permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs))
for _, id := range permSubIDs {
permSub, err := d.GetDomainPermissionSubscriptionByID(ctx, id)
if err != nil {
log.Errorf(ctx, "error getting domain permission subscription %q: %v", id, err)
continue
}
// Append to return slice
permSubs = append(permSubs, permSub)
}
return permSubs, nil
}
func (d *domainDB) PutDomainPermissionSubscription(
ctx context.Context,
permSubscription *gtsmodel.DomainPermissionSubscription,
) error {
return d.state.Caches.DB.DomainPermissionSubscription.Store(
permSubscription,
func() error {
_, err := d.db.
NewInsert().
Model(permSubscription).
Exec(ctx)
return err
},
)
}
func (d *domainDB) DeleteDomainPermissionSubscription(
ctx context.Context,
id string,
) error {
// Delete the permSub from DB.
q := d.db.NewDelete().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_subscriptions"),
bun.Ident("domain_permission_subscription"),
).
Where(
"? = ?",
bun.Ident("domain_permission_subscription.id"),
id,
)
_, err := q.Exec(ctx)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
// Invalidate any cached model by ID.
d.state.Caches.DB.DomainPermissionSubscription.Invalidate("ID", id)
return nil
}

View file

@ -0,0 +1,94 @@
// 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"
"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 {
// Create `domain_permission_drafts`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionDraft)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create `domain_permission_subscriptions`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionSubscription)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create `domain_permission_ignores`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionIgnore)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create indexes. Indices. Indie sexes.
for table, indexes := range map[string]map[string][]string{
"domain_permission_drafts": {
"domain_permission_drafts_domain_idx": {"domain"},
"domain_permission_drafts_subscription_id_idx": {"subscription_id"},
},
"domain_permission_subscriptions": {
"domain_permission_subscriptions_permission_type_idx": {"permission_type"},
},
} {
for index, columns := range indexes {
if _, err := tx.
NewCreateIndex().
Table(table).
Index(index).
Column(columns...).
IfNotExists().
Exec(ctx); err != nil {
return err
}
}
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -22,6 +22,7 @@
"net/url"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/paging"
)
// Domain contains DB functions related to domains and domain blocks.
@ -42,6 +43,9 @@ type Domain interface {
// GetDomainAllows returns all instance-level domain allows currently enforced by this instance.
GetDomainAllows(ctx context.Context) ([]*gtsmodel.DomainAllow, error)
// UpdateDomainAllow updates the given domain allow, setting the provided columns (empty for all).
UpdateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow, columns ...string) error
// DeleteDomainAllow deletes an instance-level domain allow with the given domain, if it exists.
DeleteDomainAllow(ctx context.Context, domain string) error
@ -57,6 +61,9 @@ type Domain interface {
// GetDomainBlocks returns all instance-level domain blocks currently enforced by this instance.
GetDomainBlocks(ctx context.Context) ([]*gtsmodel.DomainBlock, error)
// UpdateDomainBlock updates the given domain block, setting the provided columns (empty for all).
UpdateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock, columns ...string) error
// DeleteDomainBlock deletes an instance-level domain block with the given domain, if it exists.
DeleteDomainBlock(ctx context.Context, domain string) error
@ -78,4 +85,69 @@ type Domain interface {
// AreURIsBlocked calls IsURIBlocked for each URI.
// Will return true if even one of the given URIs is blocked.
AreURIsBlocked(ctx context.Context, uris []*url.URL) (bool, error)
/*
Domain permission draft stuff.
*/
// GetDomainPermissionDraftByID gets one DomainPermissionDraft with the given ID.
GetDomainPermissionDraftByID(ctx context.Context, id string) (*gtsmodel.DomainPermissionDraft, error)
// GetDomainPermissionDrafts returns a page of
// DomainPermissionDrafts using the given parameters.
GetDomainPermissionDrafts(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
permSubID string,
domain string,
page *paging.Page,
) ([]*gtsmodel.DomainPermissionDraft, error)
// PutDomainPermissionDraft stores one DomainPermissionDraft.
PutDomainPermissionDraft(ctx context.Context, permDraft *gtsmodel.DomainPermissionDraft) error
// DeleteDomainPermissionDraft deletes one DomainPermissionDraft with the given id.
DeleteDomainPermissionDraft(ctx context.Context, id string) error
/*
Domain permission ignore stuff.
*/
// GetDomainPermissionIgnoreByID gets one DomainPermissionIgnore with the given ID.
GetDomainPermissionIgnoreByID(ctx context.Context, id string) (*gtsmodel.DomainPermissionIgnore, error)
// GetDomainPermissionIgnores returns a page of
// DomainPermissionIgnores using the given parameters.
GetDomainPermissionIgnores(
ctx context.Context,
domain string,
page *paging.Page,
) ([]*gtsmodel.DomainPermissionIgnore, error)
// PutDomainPermissionIgnore stores one DomainPermissionIgnore.
PutDomainPermissionIgnore(ctx context.Context, permIgnore *gtsmodel.DomainPermissionIgnore) error
// DeleteDomainPermissionIgnore deletes one DomainPermissionIgnore with the given id.
DeleteDomainPermissionIgnore(ctx context.Context, id string) error
/*
Domain permission subscription stuff.
*/
// GetDomainPermissionSubscriptionByID gets one DomainPermissionSubscription with the given ID.
GetDomainPermissionSubscriptionByID(ctx context.Context, id string) (*gtsmodel.DomainPermissionSubscription, error)
// GetDomainPermissionSubscriptions returns a page of
// DomainPermissionSubscriptions using the given parameters.
GetDomainPermissionSubscriptions(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
page *paging.Page,
) ([]*gtsmodel.DomainPermissionSubscription, error)
// PutDomainPermissionSubscription stores one DomainPermissionSubscription.
PutDomainPermissionSubscription(ctx context.Context, permSub *gtsmodel.DomainPermissionSubscription) error
// DeleteDomainPermissionSubscription deletes one DomainPermissionSubscription with the given id.
DeleteDomainPermissionSubscription(ctx context.Context, id string) error
}

View file

@ -45,6 +45,10 @@ func (d *DomainAllow) GetUpdatedAt() time.Time {
return d.UpdatedAt
}
func (d *DomainAllow) SetUpdatedAt(i time.Time) {
d.UpdatedAt = i
}
func (d *DomainAllow) GetDomain() string {
return d.Domain
}
@ -53,26 +57,50 @@ func (d *DomainAllow) GetCreatedByAccountID() string {
return d.CreatedByAccountID
}
func (d *DomainAllow) SetCreatedByAccountID(i string) {
d.CreatedByAccountID = i
}
func (d *DomainAllow) GetCreatedByAccount() *Account {
return d.CreatedByAccount
}
func (d *DomainAllow) SetCreatedByAccount(i *Account) {
d.CreatedByAccount = i
}
func (d *DomainAllow) GetPrivateComment() string {
return d.PrivateComment
}
func (d *DomainAllow) SetPrivateComment(i string) {
d.PrivateComment = i
}
func (d *DomainAllow) GetPublicComment() string {
return d.PublicComment
}
func (d *DomainAllow) SetPublicComment(i string) {
d.PublicComment = i
}
func (d *DomainAllow) GetObfuscate() *bool {
return d.Obfuscate
}
func (d *DomainAllow) SetObfuscate(i *bool) {
d.Obfuscate = i
}
func (d *DomainAllow) GetSubscriptionID() string {
return d.SubscriptionID
}
func (d *DomainAllow) SetSubscriptionID(i string) {
d.SubscriptionID = i
}
func (d *DomainAllow) GetType() DomainPermissionType {
return DomainPermissionAllow
}

View file

@ -45,6 +45,10 @@ func (d *DomainBlock) GetUpdatedAt() time.Time {
return d.UpdatedAt
}
func (d *DomainBlock) SetUpdatedAt(i time.Time) {
d.UpdatedAt = i
}
func (d *DomainBlock) GetDomain() string {
return d.Domain
}
@ -53,26 +57,50 @@ func (d *DomainBlock) GetCreatedByAccountID() string {
return d.CreatedByAccountID
}
func (d *DomainBlock) SetCreatedByAccountID(i string) {
d.CreatedByAccountID = i
}
func (d *DomainBlock) GetCreatedByAccount() *Account {
return d.CreatedByAccount
}
func (d *DomainBlock) SetCreatedByAccount(i *Account) {
d.CreatedByAccount = i
}
func (d *DomainBlock) GetPrivateComment() string {
return d.PrivateComment
}
func (d *DomainBlock) SetPrivateComment(i string) {
d.PrivateComment = i
}
func (d *DomainBlock) GetPublicComment() string {
return d.PublicComment
}
func (d *DomainBlock) SetPublicComment(i string) {
d.PublicComment = i
}
func (d *DomainBlock) GetObfuscate() *bool {
return d.Obfuscate
}
func (d *DomainBlock) SetObfuscate(i *bool) {
d.Obfuscate = i
}
func (d *DomainBlock) GetSubscriptionID() string {
return d.SubscriptionID
}
func (d *DomainBlock) SetSubscriptionID(i string) {
d.SubscriptionID = i
}
func (d *DomainBlock) GetType() DomainPermissionType {
return DomainPermissionBlock
}

View file

@ -19,19 +19,26 @@
import "time"
// DomainPermission models a domain
// permission entry (block/allow).
// DomainPermission models a domain permission
// entry -- block / allow / draft / ignore.
type DomainPermission interface {
GetID() string
GetCreatedAt() time.Time
GetUpdatedAt() time.Time
SetUpdatedAt(i time.Time)
GetDomain() string
GetCreatedByAccountID() string
SetCreatedByAccountID(i string)
GetCreatedByAccount() *Account
SetCreatedByAccount(i *Account)
GetPrivateComment() string
SetPrivateComment(i string)
GetPublicComment() string
SetPublicComment(i string)
GetObfuscate() *bool
SetObfuscate(i *bool)
GetSubscriptionID() string
SetSubscriptionID(i string)
GetType() DomainPermissionType
}

View file

@ -0,0 +1,106 @@
// 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"
type DomainPermissionDraft 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"` // Time when this item was created.
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Time when this item was last updated.
PermissionType DomainPermissionType `bun:",notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"` // Permission type of the draft.
Domain string `bun:",nullzero,notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"` // Domain to block or allow. Eg. 'whatever.com'.
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this subscription.
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID.
PrivateComment string `bun:",nullzero"` // Private comment on this perm, viewable to admins.
PublicComment string `bun:",nullzero"` // Public comment on this perm, viewable (optionally) by everyone.
Obfuscate *bool `bun:",nullzero,notnull,default:false"` // Obfuscate domain name when displaying it publicly.
SubscriptionID string `bun:"type:CHAR(26),unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"` // ID of the subscription that created this draft, if any.
}
func (d *DomainPermissionDraft) GetID() string {
return d.ID
}
func (d *DomainPermissionDraft) GetCreatedAt() time.Time {
return d.CreatedAt
}
func (d *DomainPermissionDraft) GetUpdatedAt() time.Time {
return d.UpdatedAt
}
func (d *DomainPermissionDraft) SetUpdatedAt(i time.Time) {
d.UpdatedAt = i
}
func (d *DomainPermissionDraft) GetDomain() string {
return d.Domain
}
func (d *DomainPermissionDraft) GetCreatedByAccountID() string {
return d.CreatedByAccountID
}
func (d *DomainPermissionDraft) SetCreatedByAccountID(i string) {
d.CreatedByAccountID = i
}
func (d *DomainPermissionDraft) GetCreatedByAccount() *Account {
return d.CreatedByAccount
}
func (d *DomainPermissionDraft) SetCreatedByAccount(i *Account) {
d.CreatedByAccount = i
}
func (d *DomainPermissionDraft) GetPrivateComment() string {
return d.PrivateComment
}
func (d *DomainPermissionDraft) SetPrivateComment(i string) {
d.PrivateComment = i
}
func (d *DomainPermissionDraft) GetPublicComment() string {
return d.PublicComment
}
func (d *DomainPermissionDraft) SetPublicComment(i string) {
d.PublicComment = i
}
func (d *DomainPermissionDraft) GetObfuscate() *bool {
return d.Obfuscate
}
func (d *DomainPermissionDraft) SetObfuscate(i *bool) {
d.Obfuscate = i
}
func (d *DomainPermissionDraft) GetSubscriptionID() string {
return d.SubscriptionID
}
func (d *DomainPermissionDraft) SetSubscriptionID(i string) {
d.SubscriptionID = i
}
func (d *DomainPermissionDraft) GetType() DomainPermissionType {
return d.PermissionType
}

View file

@ -0,0 +1,92 @@
// 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"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// DomainPermissionIgnore represents one domain that should be ignored
// when domain permission (ignores) are created from subscriptions.
type DomainPermissionIgnore 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"` // Time when this item was created.
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Time when this item was last updated.
Domain string `bun:",nullzero,notnull,unique"` // Domain to ignore. Eg. 'whatever.com'.
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this ignore.
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID.
PrivateComment string `bun:",nullzero"` // Private comment on this ignore, viewable to admins.
}
func (d *DomainPermissionIgnore) GetID() string {
return d.ID
}
func (d *DomainPermissionIgnore) GetCreatedAt() time.Time {
return d.CreatedAt
}
func (d *DomainPermissionIgnore) GetUpdatedAt() time.Time {
return d.UpdatedAt
}
func (d *DomainPermissionIgnore) SetUpdatedAt(i time.Time) {
d.UpdatedAt = i
}
func (d *DomainPermissionIgnore) GetDomain() string {
return d.Domain
}
func (d *DomainPermissionIgnore) GetCreatedByAccountID() string {
return d.CreatedByAccountID
}
func (d *DomainPermissionIgnore) SetCreatedByAccountID(i string) {
d.CreatedByAccountID = i
}
func (d *DomainPermissionIgnore) GetCreatedByAccount() *Account {
return d.CreatedByAccount
}
func (d *DomainPermissionIgnore) SetCreatedByAccount(i *Account) {
d.CreatedByAccount = i
}
func (d *DomainPermissionIgnore) GetPrivateComment() string {
return d.PrivateComment
}
func (d *DomainPermissionIgnore) SetPrivateComment(i string) {
d.PrivateComment = i
}
/*
Stubbed functions for interface purposes.
*/
func (d *DomainPermissionIgnore) GetPublicComment() string { return "" }
func (d *DomainPermissionIgnore) SetPublicComment(_ string) {}
func (d *DomainPermissionIgnore) GetObfuscate() *bool { return util.Ptr(false) }
func (d *DomainPermissionIgnore) SetObfuscate(_ *bool) {}
func (d *DomainPermissionIgnore) GetSubscriptionID() string { return "" }
func (d *DomainPermissionIgnore) SetSubscriptionID(_ string) {}
func (d *DomainPermissionIgnore) GetType() DomainPermissionType { return DomainPermissionUnknown }

View file

@ -0,0 +1,38 @@
// 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"
type DomainPermissionSubscription 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"` // Time when this item was created.
Title string `bun:",nullzero"` // Moderator-set title for this list.
PermissionType DomainPermissionType `bun:",notnull"` // Permission type of the subscription.
AsDraft *bool `bun:",nullzero,notnull,default:true"` // Create domain permission entries resulting from this subscription as drafts.
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this subscription.
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID.
ContentType string `bun:",nullzero,notnull"` // Content type to expect from the URI.
URI string `bun:",unique,nullzero,notnull"` // URI of the domain permission list.
FetchUsername string `bun:",nullzero"` // Username to send when doing a GET of URI using basic auth.
FetchPassword string `bun:",nullzero"` // Password to send when doing a GET of URI using basic auth.
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // Time when fetch of URI was last attempted.
IsError *bool `bun:",nullzero,notnull,default:false"` // True if last fetch attempt of URI resulted in an error.
Error string `bun:",nullzero"` // If IsError=true, this field contains the error resulting from the attempted fetch.
Count uint64 `bun:""` // Count of domain permission entries discovered at URI.
}

View file

@ -31,24 +31,6 @@
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// apiDomainPerm is a cheeky shortcut for returning
// the API version of the given domain permission
// (*gtsmodel.DomainBlock or *gtsmodel.DomainAllow),
// or an appropriate error if something goes wrong.
func (p *Processor) apiDomainPerm(
ctx context.Context,
domainPermission gtsmodel.DomainPermission,
export bool,
) (*apimodel.DomainPermission, gtserror.WithCode) {
apiDomainPerm, err := p.converter.DomainPermToAPIDomainPerm(ctx, domainPermission, export)
if err != nil {
err := gtserror.NewfAt(3, "error converting domain permission to api model: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return apiDomainPerm, nil
}
// DomainPermissionCreate creates an instance-level permission
// targeting the given domain, and then processes any side
// effects of the permission creation.

View file

@ -0,0 +1,325 @@
// 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"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/paging"
)
// DomainPermissionDraftGet returns one
// domain permission draft with the given id.
func (p *Processor) DomainPermissionDraftGet(
ctx context.Context,
id string,
) (*apimodel.DomainPermission, gtserror.WithCode) {
permDraft, err := p.state.DB.GetDomainPermissionDraftByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission draft %s: %w", id, err)
return nil, gtserror.NewErrorInternalError(err)
}
if permDraft == nil {
err := fmt.Errorf("domain permission draft %s not found", id)
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
return p.apiDomainPerm(ctx, permDraft, false)
}
// DomainPermissionDraftsGet returns a page of
// DomainPermissionDrafts with the given parameters.
func (p *Processor) DomainPermissionDraftsGet(
ctx context.Context,
subscriptionID string,
domain string,
permType gtsmodel.DomainPermissionType,
page *paging.Page,
) (*apimodel.PageableResponse, gtserror.WithCode) {
permDrafts, err := p.state.DB.GetDomainPermissionDrafts(
ctx,
permType,
subscriptionID,
domain,
page,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
count := len(permDrafts)
if count == 0 {
return paging.EmptyResponse(), nil
}
// Get the lowest and highest
// ID values, used for paging.
lo := permDrafts[count-1].ID
hi := permDrafts[0].ID
// Convert each perm draft to API model.
items := make([]any, len(permDrafts))
for i, permDraft := range permDrafts {
apiPermDraft, err := p.apiDomainPerm(ctx, permDraft, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
items[i] = apiPermDraft
}
// Assemble next/prev page queries.
query := make(url.Values, 3)
if subscriptionID != "" {
query.Set(apiutil.DomainPermissionSubscriptionIDKey, subscriptionID)
}
if domain != "" {
query.Set(apiutil.DomainPermissionDomainKey, domain)
}
if permType != gtsmodel.DomainPermissionUnknown {
query.Set(apiutil.DomainPermissionPermTypeKey, permType.String())
}
return paging.PackageResponse(paging.ResponseParams{
Items: items,
Path: "/api/v1/admin/domain_permission_drafts",
Next: page.Next(lo, hi),
Prev: page.Prev(lo, hi),
Query: query,
}), nil
}
func (p *Processor) DomainPermissionDraftCreate(
ctx context.Context,
acct *gtsmodel.Account,
domain string,
permType gtsmodel.DomainPermissionType,
obfuscate bool,
publicComment string,
privateComment string,
) (*apimodel.DomainPermission, gtserror.WithCode) {
permDraft := &gtsmodel.DomainPermissionDraft{
ID: id.NewULID(),
PermissionType: permType,
Domain: domain,
CreatedByAccountID: acct.ID,
CreatedByAccount: acct,
PrivateComment: privateComment,
PublicComment: publicComment,
Obfuscate: &obfuscate,
}
if err := p.state.DB.PutDomainPermissionDraft(ctx, permDraft); err != nil {
if errors.Is(err, db.ErrAlreadyExists) {
const text = "a domain permission draft already exists with this permission type, domain, and subscription ID"
err := fmt.Errorf("%w: %s", err, text)
return nil, gtserror.NewErrorConflict(err, text)
}
// Real error.
err := gtserror.Newf("db error putting domain permission draft: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return p.apiDomainPerm(ctx, permDraft, false)
}
func (p *Processor) DomainPermissionDraftAccept(
ctx context.Context,
acct *gtsmodel.Account,
id string,
overwrite bool,
) (*apimodel.DomainPermission, string, gtserror.WithCode) {
permDraft, err := p.state.DB.GetDomainPermissionDraftByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission draft %s: %w", id, err)
return nil, "", gtserror.NewErrorInternalError(err)
}
if permDraft == nil {
err := fmt.Errorf("domain permission draft %s not found", id)
return nil, "", gtserror.NewErrorNotFound(err, err.Error())
}
var (
// Existing permission
// entry, if it exists.
existing gtsmodel.DomainPermission
)
// Check if existing entry.
switch permDraft.PermissionType {
case gtsmodel.DomainPermissionBlock:
existing, err = p.state.DB.GetDomainBlock(
gtscontext.SetBarebones(ctx),
permDraft.Domain,
)
case gtsmodel.DomainPermissionAllow:
existing, err = p.state.DB.GetDomainAllow(
gtscontext.SetBarebones(ctx),
permDraft.Domain,
)
}
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission %s: %w", id, err)
return nil, "", gtserror.NewErrorInternalError(err)
}
// Function to clean up the accepted draft, only called if
// creating or updating permission from draft is successful.
deleteDraft := func() {
if err := p.state.DB.DeleteDomainPermissionDraft(ctx, permDraft.ID); err != nil {
log.Errorf(ctx, "db error deleting domain permission draft: %v", err)
}
}
switch {
// Easy case, we just need to create a new domain
// permission from the draft, and then delete it.
case existing == nil:
var (
new *apimodel.DomainPermission
actionID string
errWithCode gtserror.WithCode
)
if permDraft.PermissionType == gtsmodel.DomainPermissionBlock {
new, actionID, errWithCode = p.createDomainBlock(
ctx,
acct,
permDraft.Domain,
*permDraft.Obfuscate,
permDraft.PublicComment,
permDraft.PrivateComment,
permDraft.SubscriptionID,
)
}
if permDraft.PermissionType == gtsmodel.DomainPermissionAllow {
new, actionID, errWithCode = p.createDomainAllow(
ctx,
acct,
permDraft.Domain,
*permDraft.Obfuscate,
permDraft.PublicComment,
permDraft.PrivateComment,
permDraft.SubscriptionID,
)
}
// Clean up the draft
// before returning.
deleteDraft()
return new, actionID, errWithCode
// Domain permission exists but we should overwrite
// it by just updating the existing domain permission.
// Domain can't change, so no need to re-run side effects.
case overwrite:
existing.SetCreatedByAccountID(permDraft.CreatedByAccountID)
existing.SetCreatedByAccount(permDraft.CreatedByAccount)
existing.SetPrivateComment(permDraft.PrivateComment)
existing.SetPublicComment(permDraft.PublicComment)
existing.SetObfuscate(permDraft.Obfuscate)
existing.SetSubscriptionID(permDraft.SubscriptionID)
var err error
switch dp := existing.(type) {
case *gtsmodel.DomainBlock:
err = p.state.DB.UpdateDomainBlock(ctx, dp)
case *gtsmodel.DomainAllow:
err = p.state.DB.UpdateDomainAllow(ctx, dp)
}
if err != nil {
err := gtserror.Newf("db error updating existing domain permission: %w", err)
return nil, "", gtserror.NewErrorInternalError(err)
}
// Clean up the draft
// before returning.
deleteDraft()
apiPerm, errWithCode := p.apiDomainPerm(ctx, existing, false)
return apiPerm, "", errWithCode
// Domain permission exists and we shouldn't
// overwrite it, leave everything alone.
default:
const text = "a domain permission already exists with this permission type and domain"
err := fmt.Errorf("%w: %s", err, text)
return nil, "", gtserror.NewErrorConflict(err, text)
}
}
func (p *Processor) DomainPermissionDraftRemove(
ctx context.Context,
acct *gtsmodel.Account,
id string,
ignoreTarget bool,
) (*apimodel.DomainPermission, gtserror.WithCode) {
permDraft, err := p.state.DB.GetDomainPermissionDraftByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission draft %s: %w", id, err)
return nil, gtserror.NewErrorInternalError(err)
}
if permDraft == nil {
err := fmt.Errorf("domain permission draft %s not found", id)
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
// Delete the permission draft.
if err := p.state.DB.DeleteDomainPermissionDraft(ctx, permDraft.ID); err != nil {
err := gtserror.Newf("db error deleting domain permission draft: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
if ignoreTarget {
// Add a domain permission ignore
// targeting the permDraft's domain.
_, err = p.DomainPermissionIgnoreCreate(
ctx,
acct,
permDraft.Domain,
permDraft.PrivateComment,
)
if err != nil && !errors.Is(err, db.ErrAlreadyExists) {
err := gtserror.Newf("db error creating domain permission ignore: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
}
return p.apiDomainPerm(ctx, permDraft, false)
}

View file

@ -0,0 +1,134 @@
// 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"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"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/paging"
)
func (p *Processor) DomainPermissionIgnoreCreate(
ctx context.Context,
acct *gtsmodel.Account,
domain string,
privateComment string,
) (*apimodel.DomainPermission, gtserror.WithCode) {
permIgnore := &gtsmodel.DomainPermissionIgnore{
ID: id.NewULID(),
Domain: domain,
CreatedByAccountID: acct.ID,
CreatedByAccount: acct,
PrivateComment: privateComment,
}
if err := p.state.DB.PutDomainPermissionIgnore(ctx, permIgnore); err != nil {
if errors.Is(err, db.ErrAlreadyExists) {
const text = "a domain permission ignore already exists with this permission type and domain"
err := fmt.Errorf("%w: %s", err, text)
return nil, gtserror.NewErrorConflict(err, text)
}
// Real error.
err := gtserror.Newf("db error putting domain permission ignore: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return p.apiDomainPerm(ctx, permIgnore, false)
}
// DomainPermissionIgnoreGet returns one
// domain permission ignore with the given id.
func (p *Processor) DomainPermissionIgnoreGet(
ctx context.Context,
id string,
) (*apimodel.DomainPermission, gtserror.WithCode) {
permIgnore, err := p.state.DB.GetDomainPermissionIgnoreByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission ignore %s: %w", id, err)
return nil, gtserror.NewErrorInternalError(err)
}
if permIgnore == nil {
err := fmt.Errorf("domain permission ignore %s not found", id)
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
return p.apiDomainPerm(ctx, permIgnore, false)
}
// DomainPermissionIgnoresGet returns a page of
// DomainPermissionIgnores with the given parameters.
func (p *Processor) DomainPermissionIgnoresGet(
ctx context.Context,
domain string,
page *paging.Page,
) (*apimodel.PageableResponse, gtserror.WithCode) {
permIgnores, err := p.state.DB.GetDomainPermissionIgnores(
ctx,
domain,
page,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
count := len(permIgnores)
if count == 0 {
return paging.EmptyResponse(), nil
}
// Get the lowest and highest
// ID values, used for paging.
lo := permIgnores[count-1].ID
hi := permIgnores[0].ID
// Convert each perm ignore to API model.
items := make([]any, len(permIgnores))
for i, permIgnore := range permIgnores {
apiPermIgnore, err := p.apiDomainPerm(ctx, permIgnore, false)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
items[i] = apiPermIgnore
}
// Assemble next/prev page queries.
query := make(url.Values, 1)
if domain != "" {
query.Set(apiutil.DomainPermissionDomainKey, domain)
}
return paging.PackageResponse(paging.ResponseParams{
Items: items,
Path: "/api/v1/admin/domain_permission_ignores",
Next: page.Next(lo, hi),
Prev: page.Prev(lo, hi),
Query: query,
}), nil
}

View file

@ -22,6 +22,7 @@
"errors"
"time"
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"
@ -97,3 +98,20 @@ func (p *Processor) rangeDomainAccounts(
}
}
}
// apiDomainPerm is a cheeky shortcut for returning
// the API version of the given domain permission,
// or an appropriate error if something goes wrong.
func (p *Processor) apiDomainPerm(
ctx context.Context,
domainPermission gtsmodel.DomainPermission,
export bool,
) (*apimodel.DomainPermission, gtserror.WithCode) {
apiDomainPerm, err := p.converter.DomainPermToAPIDomainPerm(ctx, domainPermission, export)
if err != nil {
err := gtserror.NewfAt(3, "error converting domain permission to api model: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return apiDomainPerm, nil
}

View file

@ -1937,7 +1937,8 @@ func (c *Converter) ConversationToAPIConversation(
return apiConversation, nil
}
// DomainPermToAPIDomainPerm converts a gts model domin block or allow into an api domain permission.
// DomainPermToAPIDomainPerm converts a gtsmodel domain block,
// allow, draft, or ignore into an api domain permission.
func (c *Converter) DomainPermToAPIDomainPerm(
ctx context.Context,
d gtsmodel.DomainPermission,
@ -1970,6 +1971,11 @@ func (c *Converter) DomainPermToAPIDomainPerm(
domainPerm.CreatedBy = d.GetCreatedByAccountID()
domainPerm.CreatedAt = util.FormatISO8601(d.GetCreatedAt())
// If this is a draft, also add the permission type.
if _, ok := d.(*gtsmodel.DomainPermissionDraft); ok {
domainPerm.PermissionType = d.GetType().String()
}
return domainPerm, nil
}

View file

@ -110,12 +110,19 @@ export function MenuItem(props: PropsWithChildren<MenuItemProps>) {
if (topLevel) {
classNames.push("category", "top-level");
} else {
if (thisLevel === 1 && hasChildren) {
classNames.push("category", "expanding");
} else if (thisLevel === 1 && !hasChildren) {
classNames.push("view", "expanding");
} else if (thisLevel === 2) {
classNames.push("view", "nested");
switch (true) {
case thisLevel === 1 && hasChildren:
classNames.push("category", "expanding");
break;
case thisLevel === 1 && !hasChildren:
classNames.push("view", "expanding");
break;
case thisLevel >= 2 && hasChildren:
classNames.push("nested", "category");
break;
case thisLevel >= 2 && !hasChildren:
classNames.push("nested", "view");
break;
}
}

View file

@ -0,0 +1,103 @@
/*
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/>.
*/
import { gtsApi } from "../../gts-api";
import type {
DomainPerm,
DomainPermDraftCreateParams,
DomainPermDraftSearchParams,
DomainPermDraftSearchResp,
} from "../../../types/domain-permission";
import parse from "parse-link-header";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
searchDomainPermissionDrafts: build.query<DomainPermDraftSearchResp, DomainPermDraftSearchParams>({
query: (form) => {
const params = new(URLSearchParams);
Object.entries(form).forEach(([k, v]) => {
if (v !== undefined) {
params.append(k, v);
}
});
let query = "";
if (params.size !== 0) {
query = `?${params.toString()}`;
}
return {
url: `/api/v1/admin/domain_permission_drafts${query}`
};
},
// Headers required for paging.
transformResponse: (apiResp: DomainPerm[], meta) => {
const drafts = apiResp;
const linksStr = meta?.response?.headers.get("Link");
const links = parse(linksStr);
return { drafts, links };
},
// Only provide TRANSFORMED tag id since this model is not the same
// as getDomainPermissionDraft model (due to transformResponse).
providesTags: [{ type: "DomainPermissionDraft", id: "TRANSFORMED" }]
}),
getDomainPermissionDraft: build.query<DomainPerm, string>({
query: (id) => ({
url: `/api/v1/admin/domain_permission_drafts/${id}`
}),
providesTags: (_result, _error, id) => [
{ type: 'DomainPermissionDraft', id }
],
}),
createDomainPermissionDraft: build.mutation<DomainPerm, DomainPermDraftCreateParams>({
query: (formData) => ({
method: "POST",
url: `/api/v1/admin/domain_permission_drafts`,
asForm: true,
body: formData,
discardEmpty: true
}),
invalidatesTags: [{ type: "DomainPermissionDraft", id: "TRANSFORMED" }],
}),
}),
});
/**
* View domain permission drafts.
*/
const useLazySearchDomainPermissionDraftsQuery = extended.useLazySearchDomainPermissionDraftsQuery;
/**
* Get domain permission draft with the given ID.
*/
const useGetDomainPermissionDraftQuery = extended.useGetDomainPermissionDraftQuery;
/**
* Create a domain permission draft with the given parameters.
*/
const useCreateDomainPermissionDraftMutation = extended.useCreateDomainPermissionDraftMutation;
export {
useLazySearchDomainPermissionDraftsQuery,
useGetDomainPermissionDraftQuery,
useCreateDomainPermissionDraftMutation,
};

View file

@ -37,6 +37,12 @@ const extended = gtsApi.injectEndpoints({
}),
transformResponse: listToKeyedObject<DomainPerm>("domain"),
}),
domainPermissionDrafts: build.query<any, void>({
query: () => ({
url: `/api/v1/admin/domain_permission_drafts`
}),
}),
}),
});

View file

@ -24,7 +24,7 @@ import {
type DomainPerm,
type ImportDomainPermsParams,
type MappedDomainPerms,
isDomainPermInternalKey,
stripOnImport,
} from "../../../types/domain-permission";
import { listToKeyedObject } from "../../transforms";
@ -83,7 +83,7 @@ function importEntriesProcessor(formData: ImportDomainPermsParams): (_entry: Dom
// Unset all internal processing keys
// and any undefined keys on this entry.
Object.entries(entry).forEach(([key, val]: [keyof DomainPerm, any]) => {
if (val == undefined || isDomainPermInternalKey(key)) {
if (val == undefined || stripOnImport(key)) {
delete entry[key];
}
});

View file

@ -169,6 +169,7 @@ export const gtsApi = createApi({
"HTTPHeaderBlocks",
"DefaultInteractionPolicies",
"InteractionRequest",
"DomainPermissionDraft",
],
endpoints: (build) => ({
instanceV1: build.query<InstanceV1, void>({

View file

@ -19,11 +19,12 @@
import typia from "typia";
import { PermType } from "./perm";
import { Links } from "parse-link-header";
export const validateDomainPerms = typia.createValidate<DomainPerm[]>();
/**
* A single domain permission entry (block or allow).
* A single domain permission entry (block, allow, draft, ignore).
*/
export interface DomainPerm {
id?: string;
@ -32,11 +33,14 @@ export interface DomainPerm {
private_comment?: string;
public_comment?: string;
created_at?: string;
created_by?: string;
subscription_id?: string;
// Internal processing keys; remove
// before serdes of domain perm.
// Keys that should be stripped before
// sending the domain permission (if imported).
permission_type?: PermType;
key?: string;
permType?: PermType;
suggest?: string;
valid?: boolean;
checked?: boolean;
@ -53,9 +57,9 @@ export interface MappedDomainPerms {
[key: string]: DomainPerm;
}
const domainPermInternalKeys: Set<keyof DomainPerm> = new Set([
const domainPermStripOnImport: Set<keyof DomainPerm> = new Set([
"key",
"permType",
"permission_type",
"suggest",
"valid",
"checked",
@ -65,15 +69,14 @@ const domainPermInternalKeys: Set<keyof DomainPerm> = new Set([
]);
/**
* Returns true if provided DomainPerm Object key is
* "internal"; ie., it's just for our use, and it shouldn't
* be serialized to or deserialized from the GtS API.
* Returns true if provided DomainPerm Object key is one
* that should be stripped when importing a domain permission.
*
* @param key
* @returns
*/
export function isDomainPermInternalKey(key: keyof DomainPerm) {
return domainPermInternalKeys.has(key);
export function stripOnImport(key: keyof DomainPerm) {
return domainPermStripOnImport.has(key);
}
export interface ImportDomainPermsParams {
@ -94,3 +97,102 @@ export interface ExportDomainPermsParams {
action: "export" | "export-file";
exportType: "json" | "csv" | "plain";
}
/**
* Parameters for GET to /api/v1/admin/domain_permission_drafts.
*/
export interface DomainPermDraftSearchParams {
/**
* Show only drafts created by the given subscription ID.
*/
subscription_id?: string;
/**
* Return only drafts that target the given domain.
*/
domain?: string;
/**
* Filter on "block" or "allow" type drafts.
*/
permission_type?: PermType;
/**
* Return only items *OLDER* than the given max ID (for paging downwards).
* The item with the specified ID will not be included in the response.
*/
max_id?: string;
/**
* Return only items *NEWER* than the given since ID.
* The item with the specified ID will not be included in the response.
*/
since_id?: string;
/**
* Return only items immediately *NEWER* than the given min ID (for paging upwards).
* The item with the specified ID will not be included in the response.
*/
min_id?: string;
/**
* Number of items to return.
*/
limit?: number;
}
/**
* Parameters for POST to /api/v1/admin/domain_permission_drafts/{id}/accept.
*/
export interface DomainPermDraftAcceptParams {
/**
* ID of the domain permission draft.
*/
id: string;
/**
* If a domain permission already exists with the same domain and permission
* type as the draft, overwrite the existing permission with fields from the draft.
*/
overwrite?: boolean;
}
/**
* Parameters for POST to /api/v1/admin/domain_permission_drafts/{id}/accept.
*/
export interface DomainPermDraftRejectParams {
/**
* ID of the domain permission draft.
*/
id: string;
/**
* When removing the domain permission draft, also create a domain ignore entry for
* the target domain, so that drafts will not be created for this domain in the future.
*/
ignore_target?: boolean;
}
export interface DomainPermDraftSearchResp {
drafts: DomainPerm[];
links: Links | null;
}
export interface DomainPermDraftCreateParams {
/**
* Domain to create the permission draft for.
*/
domain: string;
/**
* Create a draft "allow" or a draft "block".
*/
permission_type: PermType;
/**
* Obfuscate the name of the domain when serving it publicly.
* Eg., `example.org` becomes something like `ex***e.org`.
*/
obfuscate?: boolean;
/**
* Public comment about this domain permission. This will be displayed
* alongside the domain permission if you choose to share permissions.
*/
public_comment?: string;
/**
* Private comment about this domain permission.
* Will only be shown to other admins, so this is a useful way of
* internally keeping track of why a certain domain ended up permissioned.
*/
private_comment?: string;
}

View file

@ -0,0 +1,48 @@
/*
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/>.
*/
import isValidDomain from "is-valid-domain";
/**
* Validate the "domain" field of a form.
* @param domain
* @returns
*/
export function formDomainValidator(domain: string): string {
if (domain.length === 0) {
return "";
}
if (domain[domain.length-1] === ".") {
return "invalid domain";
}
const valid = isValidDomain(domain, {
subdomain: true,
wildcard: false,
allowUnicode: true,
topLevel: false,
});
if (valid) {
return "";
}
return "invalid domain";
}

View file

@ -194,7 +194,8 @@ nav.menu-tree {
}
}
li.nested { /* any deeper nesting, just has indent */
/* Deeper nesting. */
li.nested {
a.title {
padding-left: 1rem;
font-weight: normal;
@ -210,11 +211,35 @@ nav.menu-tree {
background: $settings-nav-bg-hover;
}
}
&.active > a.title {
color: $fg-accent;
font-weight: bold;
}
&.active {
a.title {
color: $fg-accent;
font-weight: bold;
&.category {
& > a.title {
&::after {
content: "▶";
left: 0.8rem;
bottom: 0.1rem;
position: relative;
}
}
&.active {
& > a.title {
&::after {
content: "▼";
bottom: 0;
}
border-bottom: 0.1rem dotted $gray1;
}
}
li.nested > a.title {
padding-left: 2rem;
}
}
}
@ -1334,6 +1359,50 @@ button.tab-button {
}
}
.domain-permission-drafts-view {
.domain-permission-draft {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
&.block {
border-left: 0.3rem solid $error3;
}
&.allow {
border-left: 0.3rem solid $green1;
}
&:hover {
border-color: $fg-accent;
}
.info-list {
border: none;
.info-list-entry {
background: none;
padding: 0;
}
}
}
}
.domain-permission-draft-details {
.info-list {
margin-top: 1rem;
}
}
.domain-permission-drafts-view,
.domain-permission-draft-details {
dd.permission-type {
display: flex;
gap: 0.35rem;
align-items: center;
}
}
.instance-rules {
list-style-position: inside;
margin: 0;

View file

@ -27,7 +27,7 @@ import MutationButton from "../../../../components/form/mutation-button";
import { useLocation, useSearch } from "wouter";
import { AdminAccount } from "../../../../lib/types/account";
import Username from "../../../../components/username";
import isValidDomain from "is-valid-domain";
import { formDomainValidator } from "../../../../lib/util/formvalidators";
export function AccountSearchForm() {
const [ location, setLocation ] = useLocation();
@ -45,28 +45,7 @@ export function AccountSearchForm() {
display_name: useTextInput("display_name", { defaultValue: urlQueryParams.get("display_name") ?? ""}),
by_domain: useTextInput("by_domain", {
defaultValue: urlQueryParams.get("by_domain") ?? "",
validator: (v: string) => {
if (v.length === 0) {
return "";
}
if (v[v.length-1] === ".") {
return "invalid domain";
}
const valid = isValidDomain(v, {
subdomain: true,
wildcard: false,
allowUnicode: true,
topLevel: false,
});
if (valid) {
return "";
}
return "invalid domain";
}
validator: formDomainValidator,
}),
email: useTextInput("email", { defaultValue: urlQueryParams.get("email") ?? ""}),
ip: useTextInput("ip", { defaultValue: urlQueryParams.get("ip") ?? ""}),

View file

@ -0,0 +1,151 @@
/*
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/>.
*/
import React, { useEffect, useMemo } from "react";
import { useParams } from "wouter";
import Loading from "../../../../components/loading";
import { useBaseUrl } from "../../../../lib/navigation/util";
import BackButton from "../../../../components/back-button";
import { useGetDomainPermissionDraftQuery } from "../../../../lib/query/admin/domain-permissions/drafts";
import { Error as ErrorC } from "../../../../components/error";
import Username from "../../../../components/username";
import { useLazyGetAccountQuery } from "../../../../lib/query/admin";
export default function DomainPermissionDraftDetail() {
const baseUrl = useBaseUrl();
const backLocation: String = history.state?.backLocation ?? `~${baseUrl}`;
const params = useParams();
let draftID = params.permDraftId as string | undefined;
if (!draftID) {
throw "no perm ID";
}
const {
data: permDraft,
isLoading,
isFetching,
isError,
error,
} = useGetDomainPermissionDraftQuery(draftID);
// Once we've triggered loading the perm draft,
// trigger getting the account that created it.
const [ getAccount, getAccountRes ] = useLazyGetAccountQuery();
useEffect(() => {
if (!permDraft) {
return;
}
if (!permDraft.created_by) {
return;
}
getAccount(permDraft.created_by, true);
}, [getAccount, permDraft]);
// Load the createdByAccount if possible,
// returning a username lozenge with
// a link to the account.
const createdByAccount = useMemo(() => {
const {
data: account,
isLoading: isLoadingAccount,
isFetching: isFetchingAccount,
isError: isErrorAccount,
} = getAccountRes;
// Wait for query to finish, returning
// loading spinner in the meantime.
if (isLoadingAccount || isFetchingAccount || !permDraft) {
return <Loading />;
} else if (isErrorAccount || account === undefined) {
// Fall back to account ID.
return permDraft?.created_by;
}
return (
<Username
account={account}
linkTo={`~/settings/moderation/accounts/${account.id}`}
backLocation={`~${location}`}
/>
);
}, [getAccountRes, permDraft]);
if (isLoading || isFetching) {
return <Loading />;
} else if (isError) {
return <ErrorC error={error} />;
} else if (permDraft === undefined) {
return <ErrorC error={new Error("permission draft was undefined")} />;
}
const created = permDraft.created_at ? new Date(permDraft.created_at).toDateString(): "unknown";
const domain = permDraft.domain;
const permType = permDraft.permission_type;
if (!permType) {
return <ErrorC error={new Error("permission_type was undefined")} />;
}
const publicComment = permDraft.public_comment ?? "[none]";
const privateComment = permDraft.private_comment ?? "[none]";
const subscriptionID = permDraft.subscription_id ?? "[none]";
return (
<div className="domain-permission-draft-details">
<h1><BackButton to={backLocation} /> Domain Permission Draft Detail</h1>
<dl className="info-list">
<div className="info-list-entry">
<dt>Created</dt>
<dd><time dateTime={permDraft.created_at}>{created}</time></dd>
</div>
<div className="info-list-entry">
<dt>Created By</dt>
<dd>{createdByAccount}</dd>
</div>
<div className="info-list-entry">
<dt>Domain</dt>
<dd>{domain}</dd>
</div>
<div className="info-list-entry">
<dt>Permission type</dt>
<dd className={`permission-type ${permType}`}>
<i
aria-hidden={true}
className={`fa fa-${permType === "allow" ? "check" : "close"}`}
></i>
{permType}
</dd>
</div>
<div className="info-list-entry">
<dt>Private comment</dt>
<dd>{privateComment}</dd>
</div>
<div className="info-list-entry">
<dt>Public comment</dt>
<dd>{publicComment}</dd>
</div>
<div className="info-list-entry">
<dt>Subscription ID</dt>
<dd>{subscriptionID}</dd>
</div>
</dl>
</div>
);
}

View file

@ -0,0 +1,246 @@
/*
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/>.
*/
import React, { ReactNode, useEffect, useMemo } from "react";
import { useTextInput } from "../../../../lib/form";
import { PageableList } from "../../../../components/pageable-list";
import MutationButton from "../../../../components/form/mutation-button";
import { useLocation, useSearch } from "wouter";
import { useLazySearchDomainPermissionDraftsQuery } from "../../../../lib/query/admin/domain-permissions/drafts";
import { DomainPerm } from "../../../../lib/types/domain-permission";
import { Error as ErrorC } from "../../../../components/error";
import { Select, TextInput } from "../../../../components/form/inputs";
import { formDomainValidator } from "../../../../lib/util/formvalidators";
export default function DomainPermissionDraftsSearch() {
return (
<div className="domain-permission-drafts-view">
<div className="form-section-docs">
<h1>Domain Permission Drafts</h1>
<p>
You can use the form below to search through domain permission drafts.
<br/>
Domain permission drafts are domain block or domain allow entries that are not yet in force.
<br/>
You can choose to accept or remove a draft.
</p>
</div>
<DomainPermissionDraftsSearchForm />
</div>
);
}
function DomainPermissionDraftsSearchForm() {
const [ location, setLocation ] = useLocation();
const search = useSearch();
const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]);
const hasParams = urlQueryParams.size != 0;
const [ searchDrafts, searchRes ] = useLazySearchDomainPermissionDraftsQuery();
const form = {
subscription_id: useTextInput("subscription_id", { defaultValue: urlQueryParams.get("subscription_id") ?? "" }),
domain: useTextInput("domain", {
defaultValue: urlQueryParams.get("domain") ?? "",
validator: formDomainValidator,
}),
permission_type: useTextInput("permission_type", { defaultValue: urlQueryParams.get("permission_type") ?? "" }),
limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "20" })
};
// On mount, if urlQueryParams were provided,
// trigger the search. For example, if page
// was accessed at /search?origin=local&limit=20,
// then run a search with origin=local and
// limit=20 and immediately render the results.
//
// If no urlQueryParams set, trigger default
// search (first page, no filtering).
useEffect(() => {
if (hasParams) {
searchDrafts(Object.fromEntries(urlQueryParams));
} else {
setLocation(location + "?limit=20");
}
}, [
urlQueryParams,
hasParams,
searchDrafts,
location,
setLocation,
]);
// Rather than triggering the search directly,
// the "submit" button changes the location
// based on form field params, and lets the
// useEffect hook above actually do the search.
function submitQuery(e) {
e.preventDefault();
// Parse query parameters.
const entries = Object.entries(form).map(([k, v]) => {
// Take only defined form fields.
if (v.value === undefined || v.value.length === 0 || v.value === "any") {
return null;
}
return [[k, v.value]];
}).flatMap(kv => {
// Remove any nulls.
return kv || [];
});
const searchParams = new URLSearchParams(entries);
setLocation(location + "?" + searchParams.toString());
}
// Location to return to when user clicks "back" on the detail view.
const backLocation = location + (hasParams ? `?${urlQueryParams}` : "");
// Function to map an item to a list entry.
function itemToEntry(draft: DomainPerm): ReactNode {
return (
<DraftListEntry
key={draft.id}
permDraft={draft}
linkTo={`/drafts/${draft.id}`}
backLocation={backLocation}
/>
);
}
return (
<>
<form
onSubmit={submitQuery}
// Prevent password managers
// trying to fill in fields.
autoComplete="off"
>
<Select
field={form.permission_type}
label="Permission type"
options={
<>
<option value="">Any</option>
<option value="block">Block</option>
<option value="allow">Allow</option>
</>
}
></Select>
<TextInput
field={form.domain}
label={`Domain (without "https://" prefix)`}
placeholder="example.org"
autoCapitalize="none"
spellCheck="false"
/>
<Select
field={form.limit}
label="Items per page"
options={
<>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</>
}
></Select>
<MutationButton
disabled={false}
label={"Search"}
result={searchRes}
/>
</form>
<PageableList
isLoading={searchRes.isLoading}
isFetching={searchRes.isFetching}
isSuccess={searchRes.isSuccess}
items={searchRes.data?.drafts}
itemToEntry={itemToEntry}
isError={searchRes.isError}
error={searchRes.error}
emptyMessage={<b>No drafts found that match your query.</b>}
prevNextLinks={searchRes.data?.links}
/>
</>
);
}
interface DraftEntryProps {
permDraft: DomainPerm;
linkTo: string;
backLocation: string;
}
function DraftListEntry({ permDraft, linkTo, backLocation }: DraftEntryProps) {
const [ _location, setLocation ] = useLocation();
const domain = permDraft.domain;
const permType = permDraft.permission_type;
if (!permType) {
return <ErrorC error={new Error("permission_type was undefined")} />;
}
const privateComment = permDraft.private_comment ?? "[none]";
const subscriptionID = permDraft.subscription_id ?? "[none]";
return (
<span
className={`pseudolink domain-permission-draft entry ${permType}`}
// aria-label={title}
// title={title}
onClick={() => {
// When clicking on a draft, direct
// to the detail view for that draft.
setLocation(linkTo, {
// Store the back location in history so
// the detail view can use it to return to
// this page (including query parameters).
state: { backLocation: backLocation }
});
}}
role="link"
tabIndex={0}
>
<dl className="info-list">
<div className="info-list-entry">
<dt>Domain:</dt>
<dd className="text-cutoff">{domain}</dd>
</div>
<div className="info-list-entry">
<dt>Permission type:</dt>
<dd className={`permission-type ${permType}`}>
<i
aria-hidden={true}
className={`fa fa-${permType === "allow" ? "check" : "close"}`}
></i>
{permType}
</dd>
</div>
<div className="info-list-entry">
<dt>Private comment:</dt>
<dd className="text-cutoff">{privateComment}</dd>
</div>
<div className="info-list-entry">
<dt>Subscription:</dt>
<dd className="text-cutoff">{subscriptionID}</dd>
</div>
</dl>
</span>
);
}

View file

@ -0,0 +1,108 @@
/*
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/>.
*/
import React from "react";
import useFormSubmit from "../../../../lib/form/submit";
import { useCreateDomainPermissionDraftMutation } from "../../../../lib/query/admin/domain-permissions/drafts";
import { useBoolInput, useRadioInput, useTextInput } from "../../../../lib/form";
import { formDomainValidator } from "../../../../lib/util/formvalidators";
import MutationButton from "../../../../components/form/mutation-button";
import { Checkbox, RadioGroup, TextArea, TextInput } from "../../../../components/form/inputs";
export default function DomainPermissionDraftNew() {
const form = {
domain: useTextInput("domain", {
validator: formDomainValidator,
}),
permission_type: useRadioInput("permission_type", {
options: {
block: "Block domain",
allow: "Allow domain",
}
}),
obfuscate: useBoolInput("obfuscate"),
public_comment: useTextInput("public_comment"),
private_comment: useTextInput("private_comment"),
};
const [formSubmit, result] = useFormSubmit(form, useCreateDomainPermissionDraftMutation());
return (
<form
onSubmit={formSubmit}
// Prevent password managers
// trying to fill in fields.
autoComplete="off"
>
<div className="form-section-docs">
<h2>New Domain Permission Draft</h2>
<p>
You can use the form below to create a new domain permission draft.
<br/>
Domain permission drafts are domain block or domain allow entries that are not yet in force.
<br/>
You can choose to accept or remove a draft.
</p>
</div>
<RadioGroup
field={form.permission_type}
/>
<TextInput
field={form.domain}
label={`Domain (without "https://" prefix)`}
placeholder="example.org"
autoCapitalize="none"
spellCheck="false"
/>
<TextArea
field={form.private_comment}
label={"Private comment"}
placeholder="This domain is like unto a clown car full of clowns, I suggest we block it forthwith."
autoCapitalize="sentences"
rows={3}
/>
<TextArea
field={form.public_comment}
label={"Public comment"}
placeholder="Bad posters"
autoCapitalize="sentences"
rows={3}
/>
<Checkbox
field={form.obfuscate}
label="Obfuscate domain in public lists"
/>
<MutationButton
label="Save"
result={result}
disabled={
!form.domain.value ||
!form.domain.valid ||
!form.permission_type.value
}
/>
</form>
);
}

View file

@ -116,6 +116,23 @@ function ModerationDomainPermsMenu() {
itemUrl="import-export"
icon="fa-floppy-o"
/>
<MenuItem
name="Drafts"
itemUrl="drafts"
defaultChild="search"
icon="fa-pencil"
>
<MenuItem
name="Search"
itemUrl="search"
icon="fa-list"
/>
<MenuItem
name="New draft"
itemUrl="new"
icon="fa-plus"
/>
</MenuItem>
</MenuItem>
);
}

View file

@ -29,6 +29,9 @@ import DomainPermDetail from "./domain-permissions/detail";
import AccountsSearch from "./accounts";
import AccountsPending from "./accounts/pending";
import AccountDetail from "./accounts/detail";
import DomainPermissionDraftsSearch from "./domain-permissions/drafts";
import DomainPermissionDraftNew from "./domain-permissions/drafts/new";
import DomainPermissionDraftDetail from "./domain-permissions/drafts/detail";
/*
EXPORTED COMPONENTS
@ -139,6 +142,9 @@ function ModerationDomainPermsRouter() {
<Switch>
<Route path="/import-export" component={ImportExport} />
<Route path="/process" component={ImportExport} />
<Route path="/drafts/search" component={DomainPermissionDraftsSearch} />
<Route path="/drafts/new" component={DomainPermissionDraftNew} />
<Route path="/drafts/:permDraftId" component={DomainPermissionDraftDetail} />
<Route path="/:permType" component={DomainPermissionsOverview} />
<Route path="/:permType/:domain" component={DomainPermDetail} />
<Route><Redirect to="/blocks"/></Route>