diff --git a/internal/api/client/admin/domainpermissiondraft.go b/internal/api/client/admin/domainpermissiondraft.go
new file mode 100644
index 000000000..b790cbc5b
--- /dev/null
+++ b/internal/api/client/admin/domainpermissiondraft.go
@@ -0,0 +1,99 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// 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 _, 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)
+}
diff --git a/internal/api/client/admin/domainpermissiondraftcreate.go b/internal/api/client/admin/domainpermissiondraftcreate.go
new file mode 100644
index 000000000..02d16fa8b
--- /dev/null
+++ b/internal/api/client/admin/domainpermissiondraftcreate.go
@@ -0,0 +1,173 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/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
+//
+// 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
+// '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)
+}
diff --git a/internal/api/client/admin/domainpermissiondrafts.go b/internal/api/client/admin/domainpermissiondrafts.go
new file mode 100644
index 000000000..f58f74c80
--- /dev/null
+++ b/internal/api/client/admin/domainpermissiondrafts.go
@@ -0,0 +1,184 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/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:
+//
+// ```
+// ; rel="next", ; 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 _, 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)
+}
diff --git a/internal/api/model/domain.go b/internal/api/model/domain.go
index ddc96ef05..0df608316 100644
--- a/internal/api/model/domain.go
+++ b/internal/api/model/domain.go
@@ -63,28 +63,74 @@ type DomainPermission struct {
CreatedAt string `json:"created_at,omitempty"`
}
+// DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
+//
+// swagger:model domainPermission
+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).
//
// swagger:ignore
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 +138,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"`
}
diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go
index 024ec028b..9f4c02aed 100644
--- a/internal/api/util/parsequery.go
+++ b/internal/api/util/parsequery.go
@@ -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 */
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index 09e505ff5..199e73c80 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -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()
diff --git a/internal/cache/db.go b/internal/cache/db.go
index dd4e8b212..f31a19e3b 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -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(
diff --git a/internal/cache/size.go b/internal/cache/size.go
index 8367e4c46..5a7688223 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -342,6 +342,35 @@ func sizeofConversation() uintptr {
}))
}
+func sizeofDomainPermissionDraft() uintptr {
+ return uintptr(size.Of(>smodel.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(>smodel.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(>smodel.Emoji{
ID: exampleID,
diff --git a/internal/config/config.go b/internal/config/config.go
index 4a40e9c13..8f48f1d86 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -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).
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 48d880e1b..827822635 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -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{
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index d25d6cca8..abc98b251 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -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()
diff --git a/internal/db/bundb/domain.go b/internal/db/bundb/domain.go
index 0d2a13b34..9d1375229 100644
--- a/internal/db/bundb/domain.go
+++ b/internal/db/bundb/domain.go
@@ -19,12 +19,18 @@
import (
"context"
+ "errors"
"net/url"
+ "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/state"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
@@ -328,3 +334,701 @@ func (d *domainDB) AreURIsBlocked(ctx context.Context, uris []*url.URL) (bool, e
}
return false, nil
}
+
+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
+}
+
+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 subscription ID.
+ if permType != nil {
+ 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
+}
+
+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,
+ permType *gtsmodel.DomainPermissionType,
+ 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 subscription ID.
+ if permType != nil {
+ q = q.Where(
+ "? = ?",
+ bun.Ident("domain_permission_ignore.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_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
+}
diff --git a/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go
new file mode 100644
index 000000000..3a7db3dc6
--- /dev/null
+++ b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go
@@ -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 .
+
+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)
+ }
+}
diff --git a/internal/db/domain.go b/internal/db/domain.go
index 3f7803d62..a9cbba590 100644
--- a/internal/db/domain.go
+++ b/internal/db/domain.go
@@ -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.
@@ -78,4 +79,48 @@ 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 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
}
diff --git a/internal/gtsmodel/domainpermissiondraft.go b/internal/gtsmodel/domainpermissiondraft.go
new file mode 100644
index 000000000..b40424553
--- /dev/null
+++ b/internal/gtsmodel/domainpermissiondraft.go
@@ -0,0 +1,78 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package gtsmodel
+
+import "time"
+
+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),nullzero,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) GetDomain() string {
+ return d.Domain
+}
+
+func (d *DomainPermissionDraft) GetCreatedByAccountID() string {
+ return d.CreatedByAccountID
+}
+
+func (d *DomainPermissionDraft) GetCreatedByAccount() *Account {
+ return d.CreatedByAccount
+}
+
+func (d *DomainPermissionDraft) GetPrivateComment() string {
+ return d.PrivateComment
+}
+
+func (d *DomainPermissionDraft) GetPublicComment() string {
+ return d.PublicComment
+}
+
+func (d *DomainPermissionDraft) GetObfuscate() *bool {
+ return d.Obfuscate
+}
+
+func (d *DomainPermissionDraft) GetSubscriptionID() string {
+ return d.SubscriptionID
+}
+
+func (d *DomainPermissionDraft) GetType() DomainPermissionType {
+ return DomainPermissionBlock
+}
diff --git a/internal/gtsmodel/domainpermissionignore.go b/internal/gtsmodel/domainpermissionignore.go
new file mode 100644
index 000000000..727bccdf3
--- /dev/null
+++ b/internal/gtsmodel/domainpermissionignore.go
@@ -0,0 +1,32 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package gtsmodel
+
+import "time"
+
+// 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.
+ PermissionType DomainPermissionType `bun:",notnull,unique:domain_permission_ignores_permission_type_domain_uniq"` // Permission type of the ignore.
+ Domain string `bun:",nullzero,notnull,unique:domain_permission_ignores_permission_type_domain_uniq"` // 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.
+}
diff --git a/internal/gtsmodel/domainpermissionsubscription.go b/internal/gtsmodel/domainpermissionsubscription.go
new file mode 100644
index 000000000..813d906da
--- /dev/null
+++ b/internal/gtsmodel/domainpermissionsubscription.go
@@ -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 .
+
+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.
+}
diff --git a/internal/processing/admin/domainpermission.go b/internal/processing/admin/domainpermission.go
index bedaf6a11..bcd5a9b43 100644
--- a/internal/processing/admin/domainpermission.go
+++ b/internal/processing/admin/domainpermission.go
@@ -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.
@@ -61,7 +43,7 @@ func (p *Processor) apiDomainPerm(
// from this call, and/or an error if something goes wrong.
func (p *Processor) DomainPermissionCreate(
ctx context.Context,
- permissionType gtsmodel.DomainPermissionType,
+ permType gtsmodel.DomainPermissionType,
adminAcct *gtsmodel.Account,
domain string,
obfuscate bool,
@@ -69,7 +51,7 @@ func (p *Processor) DomainPermissionCreate(
privateComment string,
subscriptionID string,
) (*apimodel.DomainPermission, string, gtserror.WithCode) {
- switch permissionType {
+ switch permType {
// Explicitly block a domain.
case gtsmodel.DomainPermissionBlock:
@@ -97,7 +79,7 @@ func (p *Processor) DomainPermissionCreate(
// Weeping, roaring, red-faced.
default:
- err := gtserror.Newf("unrecognized permission type %d", permissionType)
+ err := gtserror.Newf("unrecognized permission type %d", permType)
return nil, "", gtserror.NewErrorInternalError(err)
}
}
@@ -109,11 +91,11 @@ func (p *Processor) DomainPermissionCreate(
// action resulting from this call, and/or an error if something goes wrong.
func (p *Processor) DomainPermissionDelete(
ctx context.Context,
- permissionType gtsmodel.DomainPermissionType,
+ permType gtsmodel.DomainPermissionType,
adminAcct *gtsmodel.Account,
domainBlockID string,
) (*apimodel.DomainPermission, string, gtserror.WithCode) {
- switch permissionType {
+ switch permType {
// Delete explicit domain block.
case gtsmodel.DomainPermissionBlock:
@@ -134,7 +116,7 @@ func (p *Processor) DomainPermissionDelete(
// You do the hokey-cokey and you turn
// around, that's what it's all about.
default:
- err := gtserror.Newf("unrecognized permission type %d", permissionType)
+ err := gtserror.Newf("unrecognized permission type %d", permType)
return nil, "", gtserror.NewErrorInternalError(err)
}
}
@@ -152,14 +134,14 @@ func (p *Processor) DomainPermissionDelete(
// as they wish.
func (p *Processor) DomainPermissionsImport(
ctx context.Context,
- permissionType gtsmodel.DomainPermissionType,
+ permType gtsmodel.DomainPermissionType,
account *gtsmodel.Account,
domainsF *multipart.FileHeader,
) (*apimodel.MultiStatus, gtserror.WithCode) {
// Ensure known permission type.
- if permissionType != gtsmodel.DomainPermissionBlock &&
- permissionType != gtsmodel.DomainPermissionAllow {
- err := gtserror.Newf("unrecognized permission type %d", permissionType)
+ if permType != gtsmodel.DomainPermissionBlock &&
+ permType != gtsmodel.DomainPermissionAllow {
+ err := gtserror.Newf("unrecognized permission type %d", permType)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -201,7 +183,7 @@ func (p *Processor) DomainPermissionsImport(
domainPerm, _, errWithCode = p.DomainPermissionCreate(
ctx,
- permissionType,
+ permType,
account,
domain,
obfuscate,
@@ -240,7 +222,7 @@ func (p *Processor) DomainPermissionsImport(
// to an export.
func (p *Processor) DomainPermissionsGet(
ctx context.Context,
- permissionType gtsmodel.DomainPermissionType,
+ permType gtsmodel.DomainPermissionType,
account *gtsmodel.Account,
export bool,
) ([]*apimodel.DomainPermission, gtserror.WithCode) {
@@ -249,7 +231,7 @@ func (p *Processor) DomainPermissionsGet(
err error
)
- switch permissionType {
+ switch permType {
case gtsmodel.DomainPermissionBlock:
var blocks []*gtsmodel.DomainBlock
@@ -279,7 +261,7 @@ func (p *Processor) DomainPermissionsGet(
}
if err != nil {
- err := gtserror.Newf("error getting %ss: %w", permissionType.String(), err)
+ err := gtserror.Newf("error getting %ss: %w", permType.String(), err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -303,7 +285,7 @@ func (p *Processor) DomainPermissionsGet(
// suitable for writing out to an export.
func (p *Processor) DomainPermissionGet(
ctx context.Context,
- permissionType gtsmodel.DomainPermissionType,
+ permType gtsmodel.DomainPermissionType,
id string,
export bool,
) (*apimodel.DomainPermission, gtserror.WithCode) {
@@ -312,7 +294,7 @@ func (p *Processor) DomainPermissionGet(
err error
)
- switch permissionType {
+ switch permType {
case gtsmodel.DomainPermissionBlock:
domainPerm, err = p.state.DB.GetDomainBlockByID(ctx, id)
case gtsmodel.DomainPermissionAllow:
@@ -323,11 +305,11 @@ func (p *Processor) DomainPermissionGet(
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("no domain %s exists with id %s", permissionType.String(), id)
+ err = fmt.Errorf("no domain %s exists with id %s", permType.String(), id)
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
- err = gtserror.Newf("error getting domain %s with id %s: %w", permissionType.String(), id, err)
+ err = gtserror.Newf("error getting domain %s with id %s: %w", permType.String(), id, err)
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/admin/domainpermission_test.go b/internal/processing/admin/domainpermission_test.go
index 5a73693db..8c4bb2227 100644
--- a/internal/processing/admin/domainpermission_test.go
+++ b/internal/processing/admin/domainpermission_test.go
@@ -42,7 +42,7 @@ type domainPermAction struct {
// Type of permission
// to create or delete.
- permissionType gtsmodel.DomainPermissionType
+ permType gtsmodel.DomainPermissionType
// Domain to target
// with the permission.
@@ -89,9 +89,9 @@ func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) {
var actionID string
switch action.createOrDelete {
case "create":
- _, actionID = suite.createDomainPerm(action.permissionType, action.domain)
+ _, actionID = suite.createDomainPerm(action.permType, action.domain)
case "delete":
- _, actionID = suite.deleteDomainPerm(action.permissionType, action.domain)
+ _, actionID = suite.deleteDomainPerm(action.permType, action.domain)
default:
panic("createOrDelete was not 'create' or 'delete'")
}
@@ -118,16 +118,16 @@ func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) {
}
}
-// create given permissionType with default values.
+// create given permType with default values.
func (suite *DomainBlockTestSuite) createDomainPerm(
- permissionType gtsmodel.DomainPermissionType,
+ permType gtsmodel.DomainPermissionType,
domain string,
) (*apimodel.DomainPermission, string) {
ctx := context.Background()
apiPerm, actionID, errWithCode := suite.adminProcessor.DomainPermissionCreate(
ctx,
- permissionType,
+ permType,
suite.testAccounts["admin_account"],
domain,
false,
@@ -144,7 +144,7 @@ func (suite *DomainBlockTestSuite) createDomainPerm(
// delete given permission type.
func (suite *DomainBlockTestSuite) deleteDomainPerm(
- permissionType gtsmodel.DomainPermissionType,
+ permType gtsmodel.DomainPermissionType,
domain string,
) (*apimodel.DomainPermission, string) {
var (
@@ -154,7 +154,7 @@ func (suite *DomainBlockTestSuite) deleteDomainPerm(
// To delete the permission,
// first get it from the db.
- switch permissionType {
+ switch permType {
case gtsmodel.DomainPermissionBlock:
domainPermission, _ = suite.db.GetDomainBlock(ctx, domain)
case gtsmodel.DomainPermissionAllow:
@@ -170,7 +170,7 @@ func (suite *DomainBlockTestSuite) deleteDomainPerm(
// Now use the ID to delete it.
apiPerm, actionID, errWithCode := suite.adminProcessor.DomainPermissionDelete(
ctx,
- permissionType,
+ permType,
suite.testAccounts["admin_account"],
domainPermission.GetID(),
)
@@ -246,7 +246,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() {
actions: []domainPermAction{
{
createOrDelete: "create",
- permissionType: gtsmodel.DomainPermissionBlock,
+ permType: gtsmodel.DomainPermissionBlock,
domain: domain,
expected: func(_ context.Context, account *gtsmodel.Account) bool {
// Domain was blocked, so each
@@ -256,7 +256,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() {
},
{
createOrDelete: "delete",
- permissionType: gtsmodel.DomainPermissionBlock,
+ permType: gtsmodel.DomainPermissionBlock,
domain: domain,
expected: func(_ context.Context, account *gtsmodel.Account) bool {
// Domain was unblocked, so each
@@ -279,7 +279,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() {
actions: []domainPermAction{
{
createOrDelete: "create",
- permissionType: gtsmodel.DomainPermissionBlock,
+ permType: gtsmodel.DomainPermissionBlock,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Domain was blocked, so each
@@ -316,7 +316,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() {
},
{
createOrDelete: "create",
- permissionType: gtsmodel.DomainPermissionAllow,
+ permType: gtsmodel.DomainPermissionAllow,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Domain was explicitly allowed, so each
@@ -355,7 +355,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() {
},
{
createOrDelete: "delete",
- permissionType: gtsmodel.DomainPermissionAllow,
+ permType: gtsmodel.DomainPermissionAllow,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Deleting the allow now, while there's
@@ -382,7 +382,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() {
},
{
createOrDelete: "delete",
- permissionType: gtsmodel.DomainPermissionBlock,
+ permType: gtsmodel.DomainPermissionBlock,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Deleting the block now should
@@ -421,7 +421,7 @@ func (suite *DomainBlockTestSuite) TestAllowAndBlockDomain() {
actions: []domainPermAction{
{
createOrDelete: "create",
- permissionType: gtsmodel.DomainPermissionAllow,
+ permType: gtsmodel.DomainPermissionAllow,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Domain was explicitly allowed,
@@ -458,7 +458,7 @@ func (suite *DomainBlockTestSuite) TestAllowAndBlockDomain() {
},
{
createOrDelete: "create",
- permissionType: gtsmodel.DomainPermissionBlock,
+ permType: gtsmodel.DomainPermissionBlock,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Create a block. An allow existed, so
@@ -497,7 +497,7 @@ func (suite *DomainBlockTestSuite) TestAllowAndBlockDomain() {
},
{
createOrDelete: "delete",
- permissionType: gtsmodel.DomainPermissionAllow,
+ permType: gtsmodel.DomainPermissionAllow,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Deleting the allow now, while there's
diff --git a/internal/processing/admin/domainpermissiondraft.go b/internal/processing/admin/domainpermissiondraft.go
new file mode 100644
index 000000000..9f4a0aef0
--- /dev/null
+++ b/internal/processing/admin/domainpermissiondraft.go
@@ -0,0 +1,149 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package 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"
+)
+
+// 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 := >smodel.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 and domain"
+ return nil, gtserror.NewErrorConflict(errors.New(text), 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)
+}
diff --git a/internal/processing/admin/util.go b/internal/processing/admin/util.go
index c82ff2dc1..bc59a2b3b 100644
--- a/internal/processing/admin/util.go
+++ b/internal/processing/admin/util.go
@@ -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
+}