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/domainpermissiondraftaccept.go b/internal/api/client/admin/domainpermissiondraftaccept.go new file mode 100644 index 000000000..fc3355392 --- /dev/null +++ b/internal/api/client/admin/domainpermissiondraftaccept.go @@ -0,0 +1,107 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +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" +) + +// 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 +// +// 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: 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 _, 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, actionID, errWithCode := m.processor.Admin().DomainPermissionDraftAccept( + c.Request.Context(), + authed.Account, + id, + c.Form + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } +} diff --git a/internal/api/client/admin/domainpermissiondraftcreate.go b/internal/api/client/admin/domainpermissiondraftcreate.go new file mode 100644 index 000000000..9c738ef97 --- /dev/null +++ b/internal/api/client/admin/domainpermissiondraftcreate.go @@ -0,0 +1,175 @@ +// 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 +// '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) +} 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..c0f93ff19 100644 --- a/internal/db/bundb/domain.go +++ b/internal/db/bundb/domain.go @@ -19,12 +19,19 @@ import ( "context" + "errors" "net/url" + "slices" + "time" + "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" @@ -110,6 +117,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 +243,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) @@ -328,3 +395,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..070973525 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. @@ -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,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/domainallow.go b/internal/gtsmodel/domainallow.go index 2a3e53e79..7b947333b 100644 --- a/internal/gtsmodel/domainallow.go +++ b/internal/gtsmodel/domainallow.go @@ -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 } diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index 4e0b3ca65..e99fea301 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -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 } diff --git a/internal/gtsmodel/domainpermission.go b/internal/gtsmodel/domainpermission.go index 01e8fdaaa..865ab10ac 100644 --- a/internal/gtsmodel/domainpermission.go +++ b/internal/gtsmodel/domainpermission.go @@ -20,18 +20,25 @@ import "time" // DomainPermission models a domain -// permission entry (block/allow). +// permission entry (block/allow/draft). 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 } diff --git a/internal/gtsmodel/domainpermissiondraft.go b/internal/gtsmodel/domainpermissiondraft.go new file mode 100644 index 000000000..6715e603c --- /dev/null +++ b/internal/gtsmodel/domainpermissiondraft.go @@ -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 . + +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) 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 +} 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..b51c4b991 --- /dev/null +++ b/internal/processing/admin/domainpermissiondraft.go @@ -0,0 +1,277 @@ +// 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/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 := >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) +} + +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, "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, + ) + } + + 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) + } + + 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" + return nil, "", gtserror.NewErrorConflict(errors.New(text), text) + } +} 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 +}