summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2025-01-05 13:20:33 +0100
committerLibravatar GitHub <noreply@github.com>2025-01-05 13:20:33 +0100
commite9bb7ddd3aa11da5c48a75c4a600f8fe5cc1c990 (patch)
treea2897775112a821aa093b6e2686044814912001f /internal
parent[chore] Update robots.txt with more AI bots (#3634) (diff)
downloadgotosocial-e9bb7ddd3aa11da5c48a75c4a600f8fe5cc1c990.tar.xz
[feature] Create/update/remove domain permission subscriptions (#3623)
* [feature] Create/update/remove domain permission subscriptions * lint * envparsing * remove errant fmt.Println * create drafts, subs, exclude, from snapshot models * name etag column correctly * remove count column * lint
Diffstat (limited to 'internal')
-rw-r--r--internal/api/client/admin/admin.go86
-rw-r--r--internal/api/client/admin/domainpermission.go42
-rw-r--r--internal/api/client/admin/domainpermissiondraftcreate.go21
-rw-r--r--internal/api/client/admin/domainpermissiondraftsget.go2
-rw-r--r--internal/api/client/admin/domainpermissionsubscriptioncreate.go244
-rw-r--r--internal/api/client/admin/domainpermissionsubscriptionget.go104
-rw-r--r--internal/api/client/admin/domainpermissionsubscriptionremove.go143
-rw-r--r--internal/api/client/admin/domainpermissionsubscriptionsget.go177
-rw-r--r--internal/api/client/admin/domainpermissionsubscriptionspreviewget.go132
-rw-r--r--internal/api/client/admin/domainpermissionsubscriptionupdate.go254
-rw-r--r--internal/api/model/domain.go98
-rw-r--r--internal/cache/cache.go1
-rw-r--r--internal/cache/db.go34
-rw-r--r--internal/cache/size.go19
-rw-r--r--internal/config/config.go109
-rw-r--r--internal/config/defaults.go107
-rw-r--r--internal/config/helpers.gen.go31
-rw-r--r--internal/db/bundb/domainpermissionsubscription.go354
-rw-r--r--internal/db/bundb/domainpermissionsubscription_test.go99
-rw-r--r--internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude.go2
-rw-r--r--internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissiondraft.go33
-rw-r--r--internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissionexclude.go31
-rw-r--r--internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go75
-rw-r--r--internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions/domainpermissionsubscription.go38
-rw-r--r--internal/db/domain.go40
-rw-r--r--internal/gtsmodel/domainpermissionsubscription.go74
-rw-r--r--internal/id/ulid.go9
-rw-r--r--internal/processing/admin/domainpermissionsubscription.go285
-rw-r--r--internal/processing/admin/util.go16
-rw-r--r--internal/typeutils/internaltofrontend.go55
30 files changed, 2550 insertions, 165 deletions
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go
index a33a6448a..68a088b4d 100644
--- a/internal/api/client/admin/admin.go
+++ b/internal/api/client/admin/admin.go
@@ -28,43 +28,47 @@ import (
)
const (
- BasePath = "/v1/admin"
- EmojiPath = BasePath + "/custom_emojis"
- EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
- EmojiCategoriesPath = EmojiPath + "/categories"
- DomainBlocksPath = BasePath + "/domain_blocks"
- DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
- DomainAllowsPath = BasePath + "/domain_allows"
- DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
- DomainPermissionDraftsPath = BasePath + "/domain_permission_drafts"
- DomainPermissionDraftsPathWithID = DomainPermissionDraftsPath + "/:" + apiutil.IDKey
- DomainPermissionDraftAcceptPath = DomainPermissionDraftsPathWithID + "/accept"
- DomainPermissionDraftRemovePath = DomainPermissionDraftsPathWithID + "/remove"
- DomainPermissionExcludesPath = BasePath + "/domain_permission_excludes"
- DomainPermissionExcludesPathWithID = DomainPermissionExcludesPath + "/:" + apiutil.IDKey
- DomainKeysExpirePath = BasePath + "/domain_keys_expire"
- HeaderAllowsPath = BasePath + "/header_allows"
- HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
- HeaderBlocksPath = BasePath + "/header_blocks"
- HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
- AccountsV1Path = BasePath + "/accounts"
- AccountsV2Path = "/v2/admin/accounts"
- AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
- AccountsActionPath = AccountsPathWithID + "/action"
- AccountsApprovePath = AccountsPathWithID + "/approve"
- AccountsRejectPath = AccountsPathWithID + "/reject"
- MediaCleanupPath = BasePath + "/media_cleanup"
- MediaRefetchPath = BasePath + "/media_refetch"
- ReportsPath = BasePath + "/reports"
- ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
- ReportsResolvePath = ReportsPathWithID + "/resolve"
- EmailPath = BasePath + "/email"
- EmailTestPath = EmailPath + "/test"
- InstanceRulesPath = BasePath + "/instance/rules"
- InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
- DebugPath = BasePath + "/debug"
- DebugAPUrlPath = DebugPath + "/apurl"
- DebugClearCachesPath = DebugPath + "/caches/clear"
+ BasePath = "/v1/admin"
+ EmojiPath = BasePath + "/custom_emojis"
+ EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
+ EmojiCategoriesPath = EmojiPath + "/categories"
+ DomainBlocksPath = BasePath + "/domain_blocks"
+ DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
+ DomainAllowsPath = BasePath + "/domain_allows"
+ DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
+ DomainPermissionDraftsPath = BasePath + "/domain_permission_drafts"
+ DomainPermissionDraftsPathWithID = DomainPermissionDraftsPath + "/:" + apiutil.IDKey
+ DomainPermissionDraftAcceptPath = DomainPermissionDraftsPathWithID + "/accept"
+ DomainPermissionDraftRemovePath = DomainPermissionDraftsPathWithID + "/remove"
+ DomainPermissionExcludesPath = BasePath + "/domain_permission_excludes"
+ DomainPermissionExcludesPathWithID = DomainPermissionExcludesPath + "/:" + apiutil.IDKey
+ DomainPermissionSubscriptionsPath = BasePath + "/domain_permission_subscriptions"
+ DomainPermissionSubscriptionsPathWithID = DomainPermissionSubscriptionsPath + "/:" + apiutil.IDKey
+ DomainPermissionSubscriptionsPreviewPath = DomainPermissionSubscriptionsPath + "/preview"
+ DomainPermissionSubscriptionRemovePath = DomainPermissionSubscriptionsPathWithID + "/remove"
+ DomainKeysExpirePath = BasePath + "/domain_keys_expire"
+ HeaderAllowsPath = BasePath + "/header_allows"
+ HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
+ HeaderBlocksPath = BasePath + "/header_blocks"
+ HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
+ AccountsV1Path = BasePath + "/accounts"
+ AccountsV2Path = "/v2/admin/accounts"
+ AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
+ AccountsActionPath = AccountsPathWithID + "/action"
+ AccountsApprovePath = AccountsPathWithID + "/approve"
+ AccountsRejectPath = AccountsPathWithID + "/reject"
+ MediaCleanupPath = BasePath + "/media_cleanup"
+ MediaRefetchPath = BasePath + "/media_refetch"
+ ReportsPath = BasePath + "/reports"
+ ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
+ ReportsResolvePath = ReportsPathWithID + "/resolve"
+ EmailPath = BasePath + "/email"
+ EmailTestPath = EmailPath + "/test"
+ InstanceRulesPath = BasePath + "/instance/rules"
+ InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
+ DebugPath = BasePath + "/debug"
+ DebugAPUrlPath = DebugPath + "/apurl"
+ DebugClearCachesPath = DebugPath + "/caches/clear"
FilterQueryKey = "filter"
MaxShortcodeDomainKey = "max_shortcode_domain"
@@ -118,6 +122,14 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
attachHandler(http.MethodGet, DomainPermissionExcludesPathWithID, m.DomainPermissionExcludeGETHandler)
attachHandler(http.MethodDelete, DomainPermissionExcludesPathWithID, m.DomainPermissionExcludeDELETEHandler)
+ // domain permission subscriptions stuff
+ attachHandler(http.MethodPost, DomainPermissionSubscriptionsPath, m.DomainPermissionSubscriptionPOSTHandler)
+ attachHandler(http.MethodGet, DomainPermissionSubscriptionsPath, m.DomainPermissionSubscriptionsGETHandler)
+ attachHandler(http.MethodGet, DomainPermissionSubscriptionsPreviewPath, m.DomainPermissionSubscriptionsPreviewGETHandler)
+ attachHandler(http.MethodGet, DomainPermissionSubscriptionsPathWithID, m.DomainPermissionSubscriptionGETHandler)
+ attachHandler(http.MethodPatch, DomainPermissionSubscriptionsPathWithID, m.DomainPermissionSubscriptionPATCHHandler)
+ attachHandler(http.MethodPost, DomainPermissionSubscriptionRemovePath, m.DomainPermissionSubscriptionRemovePOSTHandler)
+
// header filtering administration routes
attachHandler(http.MethodGet, HeaderAllowsPathWithID, m.HeaderFilterAllowGET)
attachHandler(http.MethodGet, HeaderBlocksPathWithID, m.HeaderFilterBlockGET)
diff --git a/internal/api/client/admin/domainpermission.go b/internal/api/client/admin/domainpermission.go
index 90c0eb4c0..5138be898 100644
--- a/internal/api/client/admin/domainpermission.go
+++ b/internal/api/client/admin/domainpermission.go
@@ -302,3 +302,45 @@ func (m *Module) getDomainPermissions(
apiutil.JSON(c, http.StatusOK, domainPerm)
}
+
+// parseDomainPermissionType is a util function to parse i
+// to a DomainPermissionType, or return a suitable error.
+func parseDomainPermissionType(i string) (
+ permType gtsmodel.DomainPermissionType,
+ errWithCode gtserror.WithCode,
+) {
+ if i == "" {
+ const errText = "permission_type not set, must be one of block or allow"
+ errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ return
+ }
+
+ permType = gtsmodel.ParseDomainPermissionType(i)
+ if permType == gtsmodel.DomainPermissionUnknown {
+ var errText = fmt.Sprintf("permission_type %s not recognized, must be one of block or allow", i)
+ errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ }
+
+ return
+}
+
+// parseDomainPermSubContentType is a util function to parse i
+// to a DomainPermSubContentType, or return a suitable error.
+func parseDomainPermSubContentType(i string) (
+ contentType gtsmodel.DomainPermSubContentType,
+ errWithCode gtserror.WithCode,
+) {
+ if i == "" {
+ const errText = "content_type not set, must be one of text/csv, text/plain or application/json"
+ errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ return
+ }
+
+ contentType = gtsmodel.NewDomainPermSubContentType(i)
+ if contentType == gtsmodel.DomainPermSubContentTypeUnknown {
+ var errText = fmt.Sprintf("content_type %s not recognized, must be one of text/csv, text/plain or application/json", i)
+ errWithCode = gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ }
+
+ return
+}
diff --git a/internal/api/client/admin/domainpermissiondraftcreate.go b/internal/api/client/admin/domainpermissiondraftcreate.go
index d20842ebf..ec94f947b 100644
--- a/internal/api/client/admin/domainpermissiondraftcreate.go
+++ b/internal/api/client/admin/domainpermissiondraftcreate.go
@@ -26,7 +26,6 @@ import (
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"
)
@@ -136,24 +135,8 @@ func (m *Module) DomainPermissionDraftsPOSTHandler(c *gin.Context) {
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)
+ permType, errWithCode := parseDomainPermissionType(form.PermissionType)
+ if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
diff --git a/internal/api/client/admin/domainpermissiondraftsget.go b/internal/api/client/admin/domainpermissiondraftsget.go
index d63179afc..21ce5dc43 100644
--- a/internal/api/client/admin/domainpermissiondraftsget.go
+++ b/internal/api/client/admin/domainpermissiondraftsget.go
@@ -149,7 +149,7 @@ func (m *Module) DomainPermissionDraftsGETHandler(c *gin.Context) {
permTypeStr := c.Query(apiutil.DomainPermissionPermTypeKey)
permType := gtsmodel.ParseDomainPermissionType(permTypeStr)
- if permType == gtsmodel.DomainPermissionUnknown {
+ if permTypeStr != "" && permType == gtsmodel.DomainPermissionUnknown {
text := fmt.Sprintf(
"permission_type %s not recognized, valid values are empty string, block, or allow",
permTypeStr,
diff --git a/internal/api/client/admin/domainpermissionsubscriptioncreate.go b/internal/api/client/admin/domainpermissionsubscriptioncreate.go
new file mode 100644
index 000000000..dd0b43aca
--- /dev/null
+++ b/internal/api/client/admin/domainpermissionsubscriptioncreate.go
@@ -0,0 +1,244 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package admin
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+// DomainPermissionSubscriptionPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_subscriptions domainPermissionSubscriptionCreate
+//
+// Create a domain permission subscription with the given parameters.
+//
+// ---
+// tags:
+// - admin
+//
+// consumes:
+// - multipart/form-data
+// - application/json
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: priority
+// in: formData
+// description: >-
+// Priority of this subscription compared to others of the same permission type.
+// 0-255 (higher = higher priority). Higher priority subscriptions will overwrite
+// permissions generated by lower priority subscriptions. When two subscriptions
+// have the same `priority` value, priority is indeterminate, so it's recommended
+// to always set this value manually.
+// type: number
+// minimum: 0
+// maximum: 255
+// default: 0
+// -
+// name: title
+// in: formData
+// description: Optional title for this subscription.
+// type: string
+// -
+// name: permission_type
+// required: true
+// in: formData
+// description: >-
+// Type of permissions to create by parsing the targeted file/list.
+// One of "allow" or "block".
+// type: string
+// -
+// name: as_draft
+// in: formData
+// description: >-
+// If true, domain permissions arising from this subscription will be
+// created as drafts that must be approved by a moderator to take effect.
+// If false, domain permissions from this subscription will come into force immediately.
+// Defaults to "true".
+// type: boolean
+// default: true
+// -
+// name: adopt_orphans
+// in: formData
+// description: >-
+// If true, this domain permission subscription will "adopt" domain permissions
+// which already exist on the instance, and which meet the following conditions:
+// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
+// in the subscribed list. Such orphaned domain permissions will be given this
+// subscription's subscription ID value and be managed by this subscription.
+// type: boolean
+// default: false
+// -
+// name: uri
+// required: true
+// in: formData
+// description: URI to call in order to fetch the permissions list.
+// type: string
+// -
+// name: content_type
+// required: true
+// in: formData
+// description: >-
+// MIME content type to use when parsing the permissions list.
+// One of "text/plain", "text/csv", and "application/json".
+// type: string
+// -
+// name: fetch_username
+// in: formData
+// description: >-
+// Optional basic auth username to provide when fetching given uri.
+// If set, will be transmitted along with `fetch_password` when doing the fetch.
+// type: string
+// -
+// name: fetch_password
+// in: formData
+// description: >-
+// Optional basic auth password to provide when fetching given uri.
+// If set, will be transmitted along with `fetch_username` when doing the fetch.
+// type: string
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: The newly created domain permission subscription.
+// schema:
+// "$ref": "#/definitions/domainPermissionSubscription"
+// '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) DomainPermissionSubscriptionPOSTHandler(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.DomainPermissionSubscriptionRequest)
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ // Check priority.
+ // Default to 0.
+ priority := util.PtrOrZero(form.Priority)
+ if priority < 0 || priority > 255 {
+ const errText = "priority must be a number in the range 0 to 255"
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Ensure URI is set.
+ if form.URI == nil {
+ const errText = "uri must be set"
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Ensure URI is parseable.
+ uri, err := url.Parse(*form.URI)
+ if err != nil {
+ err := fmt.Errorf("invalid uri provided: %w", err)
+ errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Normalize URI by converting back to string.
+ uriStr := uri.String()
+
+ // Content type must be set.
+ contentTypeStr := util.PtrOrZero(form.ContentType)
+ contentType, errWithCode := parseDomainPermSubContentType(contentTypeStr)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Permission type must be set.
+ permTypeStr := util.PtrOrZero(form.PermissionType)
+ permType, errWithCode := parseDomainPermissionType(permTypeStr)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Default `as_draft` to true.
+ asDraft := util.PtrOrValue(form.AsDraft, true)
+
+ permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionCreate(
+ c.Request.Context(),
+ authed.Account,
+ uint8(priority), // #nosec G115 -- Validated above.
+ util.PtrOrZero(form.Title), // Optional.
+ uriStr,
+ contentType,
+ permType,
+ asDraft,
+ util.PtrOrZero(form.FetchUsername), // Optional.
+ util.PtrOrZero(form.FetchPassword), // Optional.
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, permSub)
+}
diff --git a/internal/api/client/admin/domainpermissionsubscriptionget.go b/internal/api/client/admin/domainpermissionsubscriptionget.go
new file mode 100644
index 000000000..841e37f24
--- /dev/null
+++ b/internal/api/client/admin/domainpermissionsubscriptionget.go
@@ -0,0 +1,104 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package admin
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// DomainPermissionSubscriptionGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions/{id} domainPermissionSubscriptionGet
+//
+// Get domain permission subscription with the given ID.
+//
+// ---
+// tags:
+// - admin
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// required: true
+// in: path
+// description: ID of the domain permission subscription.
+// type: string
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: Domain permission subscription.
+// schema:
+// "$ref": "#/definitions/domainPermissionSubscription"
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) DomainPermissionSubscriptionGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionGet(c.Request.Context(), id)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, permSub)
+}
diff --git a/internal/api/client/admin/domainpermissionsubscriptionremove.go b/internal/api/client/admin/domainpermissionsubscriptionremove.go
new file mode 100644
index 000000000..97f226a31
--- /dev/null
+++ b/internal/api/client/admin/domainpermissionsubscriptionremove.go
@@ -0,0 +1,143 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package admin
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+// DomainPermissionSubscriptionRemovePOSTHandler swagger:operation POST /api/v1/admin/domain_permission_subscriptions/{id}/remove domainPermissionSubscriptionRemove
+//
+// Remove a domain permission subscription.
+//
+// ---
+// tags:
+// - admin
+//
+// consumes:
+// - multipart/form-data
+// - application/json
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// required: true
+// in: path
+// description: ID of the domain permission subscription.
+// type: string
+// -
+// name: remove_children
+// in: formData
+// description: >-
+// When removing the domain permission subscription, also
+// remove children of this subscription, ie., domain permissions
+// that are managed by this subscription. If false, then children
+// will instead be orphaned but not removed.
+//
+// Note that removed permissions may end up being created again later
+// by another domain permission subscription of lower priority than
+// the removed subscription. Likewise, orphaned children may be later
+// adopted by another subscription.
+// type: boolean
+// default: true
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: The removed domain permission subscription.
+// schema:
+// "$ref": "#/definitions/domainPermissionSubscription"
+// '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) DomainPermissionSubscriptionRemovePOSTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ type RemoveForm struct {
+ RemoveChildren *bool `json:"remove_children" form:"remove_children"`
+ }
+
+ form := new(RemoveForm)
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ // Default removeChildren to true.
+ removeChildren := util.PtrOrValue(form.RemoveChildren, true)
+
+ permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionRemove(
+ c.Request.Context(),
+ id,
+ removeChildren,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, permSub)
+}
diff --git a/internal/api/client/admin/domainpermissionsubscriptionsget.go b/internal/api/client/admin/domainpermissionsubscriptionsget.go
new file mode 100644
index 000000000..477013ec9
--- /dev/null
+++ b/internal/api/client/admin/domainpermissionsubscriptionsget.go
@@ -0,0 +1,177 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package admin
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+)
+
+// DomainPermissionSubscriptionsGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions domainPermissionSubscriptionsGet
+//
+// View domain permission subscriptions.
+//
+// The subscriptions will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
+//
+// The next and previous queries can be parsed from the returned Link header.
+//
+// Example:
+//
+// ```
+// <https://example.org/api/v1/admin/domain_permission_subscriptions?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_subscriptions?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
+// ````
+//
+// ---
+// tags:
+// - admin
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: permission_type
+// type: string
+// description: Filter on "block" or "allow" type subscriptions.
+// 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 subscriptions.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/domainPermissionSubscription"
+// 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) DomainPermissionSubscriptionsGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ permType := c.Query(apiutil.DomainPermissionPermTypeKey)
+ switch permType {
+ case "", "block", "allow":
+ // No problem.
+
+ default:
+ // Invalid.
+ text := fmt.Sprintf(
+ "permission_type %s not recognized, valid values are empty string, block, or allow",
+ permType,
+ )
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ resp, errWithCode := m.processor.Admin().DomainPermissionSubscriptionsGet(
+ c.Request.Context(),
+ gtsmodel.ParseDomainPermissionType(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/client/admin/domainpermissionsubscriptionspreviewget.go b/internal/api/client/admin/domainpermissionsubscriptionspreviewget.go
new file mode 100644
index 000000000..dc46c159b
--- /dev/null
+++ b/internal/api/client/admin/domainpermissionsubscriptionspreviewget.go
@@ -0,0 +1,132 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package admin
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// DomainPermissionSubscriptionsPreviewGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions/preview domainPermissionSubscriptionsPreviewGet
+//
+// View all domain permission subscriptions of the given permission type, in priority order (highest to lowest).
+//
+// This view allows you to see the order in which domain permissions will actually be fetched and created.
+//
+// ---
+// tags:
+// - admin
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: permission_type
+// type: string
+// description: Filter on "block" or "allow" type subscriptions.
+// in: query
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: Domain permission subscriptions.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/domainPermissionSubscription"
+// '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) DomainPermissionSubscriptionsPreviewGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ permType := c.Query(apiutil.DomainPermissionPermTypeKey)
+ switch permType {
+ case "block", "allow":
+ // No problem.
+
+ case "":
+ // Not set.
+ const text = "permission_type must be set, valid values are block or allow"
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+
+ default:
+ // Invalid.
+ text := fmt.Sprintf(
+ "permission_type %s not recognized, valid values are block or allow",
+ permType,
+ )
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ resp, errWithCode := m.processor.Admin().DomainPermissionSubscriptionsGetByPriority(
+ c.Request.Context(),
+ gtsmodel.ParseDomainPermissionType(permType),
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, resp)
+}
diff --git a/internal/api/client/admin/domainpermissionsubscriptionupdate.go b/internal/api/client/admin/domainpermissionsubscriptionupdate.go
new file mode 100644
index 000000000..de73c4d3e
--- /dev/null
+++ b/internal/api/client/admin/domainpermissionsubscriptionupdate.go
@@ -0,0 +1,254 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package admin
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "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"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+// DomainPermissionSubscriptionPATCHHandler swagger:operation PATCH /api/v1/admin/domain_permission_subscriptions/${id} domainPermissionSubscriptionUpdate
+//
+// Update a domain permission subscription with the given parameters.
+//
+// ---
+// tags:
+// - admin
+//
+// consumes:
+// - multipart/form-data
+// - application/json
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// required: true
+// in: path
+// description: ID of the domain permission subscription.
+// type: string
+// -
+// name: priority
+// in: formData
+// description: >-
+// Priority of this subscription compared to others of the same permission type.
+// 0-255 (higher = higher priority). Higher priority subscriptions will overwrite
+// permissions generated by lower priority subscriptions. When two subscriptions
+// have the same `priority` value, priority is indeterminate, so it's recommended
+// to always set this value manually.
+// type: number
+// minimum: 0
+// maximum: 255
+// -
+// name: title
+// in: formData
+// description: Optional title for this subscription.
+// type: string
+// -
+// name: uri
+// in: formData
+// description: URI to call in order to fetch the permissions list.
+// type: string
+// -
+// name: as_draft
+// in: formData
+// description: >-
+// If true, domain permissions arising from this subscription will be
+// created as drafts that must be approved by a moderator to take effect.
+// If false, domain permissions from this subscription will come into force immediately.
+// Defaults to "true".
+// type: boolean
+// default: true
+// -
+// name: adopt_orphans
+// in: formData
+// description: >-
+// If true, this domain permission subscription will "adopt" domain permissions
+// which already exist on the instance, and which meet the following conditions:
+// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
+// in the subscribed list. Such orphaned domain permissions will be given this
+// subscription's subscription ID value and be managed by this subscription.
+// type: boolean
+// default: false
+// -
+// name: content_type
+// in: formData
+// description: >-
+// MIME content type to use when parsing the permissions list.
+// One of "text/plain", "text/csv", and "application/json".
+// type: string
+// -
+// name: fetch_username
+// in: formData
+// description: >-
+// Optional basic auth username to provide when fetching given uri.
+// If set, will be transmitted along with `fetch_password` when doing the fetch.
+// type: string
+// -
+// name: fetch_password
+// in: formData
+// description: >-
+// Optional basic auth password to provide when fetching given uri.
+// If set, will be transmitted along with `fetch_username` when doing the fetch.
+// type: string
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: The updated domain permission subscription.
+// schema:
+// "$ref": "#/definitions/domainPermissionSubscription"
+// '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) DomainPermissionSubscriptionPATCHHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Parse + validate form.
+ form := new(apimodel.DomainPermissionSubscriptionRequest)
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ // Normalize priority if set.
+ var priority *uint8
+ if form.Priority != nil {
+ prioInt := *form.Priority
+ if prioInt < 0 || prioInt > 255 {
+ const errText = "priority must be a number in the range 0 to 255"
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ priority = util.Ptr(uint8(prioInt)) // #nosec G115 -- Just validated.
+ }
+
+ // Validate URI if set.
+ var uriStr *string
+ if form.URI != nil {
+ uri, err := url.Parse(*form.URI)
+ if err != nil {
+ err := fmt.Errorf("invalid uri provided: %w", err)
+ errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Normalize URI by converting back to string.
+ uriStr = util.Ptr(uri.String())
+ }
+
+ // Validate content type if set.
+ var contentType *gtsmodel.DomainPermSubContentType
+ if form.ContentType != nil {
+ ct, errWithCode := parseDomainPermSubContentType(*form.ContentType)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ contentType = &ct
+ }
+
+ // Make sure at least one field is set,
+ // otherwise we're trying to update nothing.
+ if priority == nil &&
+ form.Title == nil &&
+ uriStr == nil &&
+ contentType == nil &&
+ form.AsDraft == nil &&
+ form.AdoptOrphans == nil &&
+ form.FetchUsername == nil &&
+ form.FetchPassword == nil {
+ const errText = "no updateable fields set on request"
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionUpdate(
+ c.Request.Context(),
+ id,
+ priority,
+ form.Title,
+ uriStr,
+ contentType,
+ form.AsDraft,
+ form.AdoptOrphans,
+ form.FetchUsername,
+ form.FetchPassword,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, permSub)
+}
diff --git a/internal/api/model/domain.go b/internal/api/model/domain.go
index c973c7d92..94a190f63 100644
--- a/internal/api/model/domain.go
+++ b/internal/api/model/domain.go
@@ -99,3 +99,101 @@ type DomainKeysExpireRequest struct {
// hostname/domain to expire keys for.
Domain string `form:"domain" json:"domain"`
}
+
+// DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
+//
+// swagger:model domainPermissionSubscription
+type DomainPermissionSubscription struct {
+ // The ID of the domain permission subscription.
+ // example: 01FBW21XJA09XYX51KV5JVBW0F
+ // readonly: true
+ ID string `json:"id"`
+ // Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
+ // example: 100
+ Priority uint8 `json:"priority"`
+ // Title of this subscription, as set by admin who created or updated it.
+ // example: really cool list of neato pals
+ Title string `json:"title"`
+ // 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"`
+ // If true, this domain permission subscription will "adopt" domain permissions which already exist on the instance, and which meet the following conditions: 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present in the subscribed list. Such orphaned domain permissions will be given this subscription's subscription ID value.
+ // example: false
+ AdoptOrphans bool `json:"adopt_orphans"`
+ // Time at which the subscription was created (ISO 8601 Datetime).
+ // example: 2021-07-30T09:20:25+00:00
+ CreatedAt string `json:"created_at"`
+ // ID of the account that created this subscription.
+ // example: 01FBW21XJA09XYX51KV5JVBW0F
+ // readonly: true
+ CreatedBy string `json:"created_by"`
+ // URI to call in order to fetch the permissions list.
+ // example: https://www.example.org/blocklists/list1.csv
+ URI string `json:"uri"`
+ // MIME content type to use when parsing the permissions list.
+ // example: text/csv
+ ContentType string `json:"content_type"`
+ // (Optional) username to set for basic auth when doing a fetch of URI.
+ // example: admin123
+ FetchUsername string `json:"fetch_username,omitempty"`
+ // (Optional) password to set for basic auth when doing a fetch of URI.
+ // example: admin123
+ FetchPassword string `json:"fetch_password,omitempty"`
+ // Time of the most recent fetch attempt (successful or otherwise) (ISO 8601 Datetime).
+ // example: 2021-07-30T09:20:25+00:00
+ // readonly: true
+ FetchedAt string `json:"fetched_at,omitempty"`
+ // Time of the most recent successful fetch (ISO 8601 Datetime).
+ // example: 2021-07-30T09:20:25+00:00
+ // readonly: true
+ SuccessfullyFetchedAt string `json:"successfully_fetched_at,omitempty"`
+ // 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,omitempty"`
+ // Count of domain permission entries discovered at URI on last (successful) fetch.
+ // example: 53
+ // readonly: true
+ Count uint64 `json:"count"`
+}
+
+// DomainPermissionSubscriptionRequest represents a request to create or update a domain permission subscription..
+//
+// swagger:ignore
+type DomainPermissionSubscriptionRequest struct {
+ // Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
+ // example: 100
+ Priority *int `form:"priority" json:"priority"`
+ // Title of this subscription, as set by admin who created or updated it.
+ // example: really cool list of neato pals
+ Title *string `form:"title" json:"title"`
+ // The type of domain permission subscription (allow, block).
+ // example: block
+ PermissionType *string `form:"permission_type" json:"permission_type"`
+ // URI to call in order to fetch the permissions list.
+ // example: https://www.example.org/blocklists/list1.csv
+ URI *string `form:"uri" json:"uri"`
+ // MIME content type to use when parsing the permissions list.
+ // example: text/csv
+ ContentType *string `form:"content_type" json:"content_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 `form:"as_draft" json:"as_draft"`
+ // If true, this domain permission subscription will "adopt" domain permissions
+ // which already exist on the instance, and which meet the following conditions:
+ // 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
+ // in the subscribed list. Such orphaned domain permissions will be given this
+ // subscription's subscription ID value and be managed by this subscription.
+ AdoptOrphans *bool `form:"adopt_orphans" json:"adopt_orphans"`
+ // (Optional) username to set for basic auth when doing a fetch of URI.
+ // example: admin123
+ FetchUsername *string `form:"fetch_username" json:"fetch_username"`
+ // (Optional) password to set for basic auth when doing a fetch of URI.
+ // example: admin123
+ FetchPassword *string `form:"fetch_password" json:"fetch_password"`
+}
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index 1a66fcd6b..560fbc9f6 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -75,6 +75,7 @@ func (c *Caches) Init() {
c.initDomainAllow()
c.initDomainBlock()
c.initDomainPermissionDraft()
+ c.initDomainPermissionSubscription()
c.initDomainPermissionExclude()
c.initEmoji()
c.initEmojiCategory()
diff --git a/internal/cache/db.go b/internal/cache/db.go
index 7a47b811f..1052446c4 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -70,6 +70,9 @@ type DBCaches struct {
// 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]
+
// DomainPermissionExclude provides access to the domain permission exclude database cache.
DomainPermissionExclude *domain.Cache
@@ -589,6 +592,37 @@ func (c *Caches) initDomainPermissionDraft() {
})
}
+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) initDomainPermissionExclude() {
c.DB.DomainPermissionExclude = new(domain.Cache)
}
diff --git a/internal/cache/size.go b/internal/cache/size.go
index 988755099..2c8772f96 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -357,6 +357,25 @@ func sizeofDomainPermissionDraft() uintptr {
}))
}
+func sizeofDomainPermissionSubscription() uintptr {
+ return uintptr(size.Of(&gtsmodel.DomainPermissionSubscription{
+ ID: exampleID,
+ Priority: uint8(255),
+ Title: exampleTextSmall,
+ PermissionType: gtsmodel.DomainPermissionBlock,
+ AsDraft: util.Ptr(true),
+ CreatedByAccountID: exampleID,
+ URI: exampleURI,
+ ContentType: gtsmodel.DomainPermSubContentTypeCSV,
+ FetchUsername: "username",
+ FetchPassword: "password",
+ FetchedAt: exampleTime,
+ SuccessfullyFetchedAt: exampleTime,
+ ETag: exampleID,
+ Error: exampleTextSmall,
+ }))
+}
+
func sizeofEmoji() uintptr {
return uintptr(size.Of(&gtsmodel.Emoji{
ID: exampleID,
diff --git a/internal/config/config.go b/internal/config/config.go
index 413743409..2bf2a77ad 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -196,60 +196,61 @@ 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"`
- DomainPermissionDraftMemRation float64 `name:"domain-permission-draft-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"`
- StatusEditMemRatio float64 `name:"status-edit-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"`
+ StatusEditMemRatio float64 `name:"status-edit-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 f77c5c456..97d96d1ba 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -158,59 +158,60 @@ var Defaults = Configuration{
// 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,
- DomainPermissionDraftMemRation: 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,
- StatusEditMemRatio: 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,
+ StatusEditMemRatio: 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 543292ebe..625c4ea78 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -3187,6 +3187,37 @@ 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/domainpermissionsubscription.go b/internal/db/bundb/domainpermissionsubscription.go
new file mode 100644
index 000000000..be22b96a3
--- /dev/null
+++ b/internal/db/bundb/domainpermissionsubscription.go
@@ -0,0 +1,354 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package bundb
+
+import (
+ "context"
+ "errors"
+ "slices"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+ "github.com/uptrace/bun"
+)
+
+func (d *domainDB) getDomainPermissionSubscription(
+ ctx context.Context,
+ lookup string,
+ dbQuery func(*gtsmodel.DomainPermissionSubscription) error,
+ keyParts ...any,
+) (*gtsmodel.DomainPermissionSubscription, error) {
+ // Fetch perm subscription from database cache with loader callback.
+ permSub, err := d.state.Caches.DB.DomainPermissionSubscription.LoadOne(
+ lookup,
+ // Only called if not cached.
+ func() (*gtsmodel.DomainPermissionSubscription, error) {
+ var permSub gtsmodel.DomainPermissionSubscription
+ if err := dbQuery(&permSub); err != nil {
+ return nil, err
+ }
+ return &permSub, nil
+ },
+ keyParts...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ if gtscontext.Barebones(ctx) {
+ // No need to fully populate.
+ return permSub, nil
+ }
+
+ if permSub.CreatedByAccount == nil {
+ // Not set, fetch from database.
+ permSub.CreatedByAccount, err = d.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ permSub.CreatedByAccountID,
+ )
+ if err != nil {
+ return nil, gtserror.Newf("error populating created by account: %w", err)
+ }
+ }
+
+ return permSub, nil
+}
+
+func (d *domainDB) GetDomainPermissionSubscriptionByID(
+ ctx context.Context,
+ id string,
+) (*gtsmodel.DomainPermissionSubscription, error) {
+ return d.getDomainPermissionSubscription(
+ ctx,
+ "ID",
+ func(permSub *gtsmodel.DomainPermissionSubscription) error {
+ return d.db.
+ NewSelect().
+ Model(permSub).
+ Where("? = ?", bun.Ident("domain_permission_subscription.id"), id).
+ Scan(ctx)
+ },
+ id,
+ )
+}
+
+func (d *domainDB) GetDomainPermissionSubscriptions(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+ page *paging.Page,
+) (
+ []*gtsmodel.DomainPermissionSubscription,
+ error,
+) {
+ var (
+ // Get paging params.
+ minID = page.GetMin()
+ maxID = page.GetMax()
+ limit = page.GetLimit()
+ order = page.GetOrder()
+
+ // Make educated guess for slice size
+ permSubIDs = make([]string, 0, limit)
+ )
+
+ q := d.db.
+ NewSelect().
+ TableExpr(
+ "? AS ?",
+ bun.Ident("domain_permission_subscriptions"),
+ bun.Ident("domain_permission_subscription"),
+ ).
+ // Select only IDs from table
+ Column("domain_permission_subscription.id")
+
+ // Return only items with id
+ // lower than provided maxID.
+ if maxID != "" {
+ q = q.Where(
+ "? < ?",
+ bun.Ident("domain_permission_subscription.id"),
+ maxID,
+ )
+ }
+
+ // Return only items with id
+ // greater than provided minID.
+ if minID != "" {
+ q = q.Where(
+ "? > ?",
+ bun.Ident("domain_permission_subscription.id"),
+ minID,
+ )
+ }
+
+ // Return only items with
+ // given permission type.
+ if permType != gtsmodel.DomainPermissionUnknown {
+ q = q.Where(
+ "? = ?",
+ bun.Ident("domain_permission_subscription.permission_type"),
+ permType,
+ )
+ }
+
+ if limit > 0 {
+ // Limit amount of
+ // items returned.
+ q = q.Limit(limit)
+ }
+
+ if order == paging.OrderAscending {
+ // Page up.
+ q = q.OrderExpr(
+ "? ASC",
+ bun.Ident("domain_permission_subscription.id"),
+ )
+ } else {
+ // Page down.
+ q = q.OrderExpr(
+ "? DESC",
+ bun.Ident("domain_permission_subscription.id"),
+ )
+ }
+
+ if err := q.Scan(ctx, &permSubIDs); err != nil {
+ return nil, err
+ }
+
+ // Catch case of no items early
+ if len(permSubIDs) == 0 {
+ return nil, db.ErrNoEntries
+ }
+
+ // If we're paging up, we still want items
+ // to be sorted by ID desc, so reverse slice.
+ if order == paging.OrderAscending {
+ slices.Reverse(permSubIDs)
+ }
+
+ // Allocate return slice (will be at most len permSubIDs).
+ permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs))
+ for _, id := range permSubIDs {
+ permSub, err := d.GetDomainPermissionSubscriptionByID(ctx, id)
+ if err != nil {
+ log.Errorf(ctx, "error getting domain permission subscription %q: %v", id, err)
+ continue
+ }
+
+ // Append to return slice
+ permSubs = append(permSubs, permSub)
+ }
+
+ return permSubs, nil
+}
+
+func (d *domainDB) GetDomainPermissionSubscriptionsByPriority(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+) (
+ []*gtsmodel.DomainPermissionSubscription,
+ error,
+) {
+ permSubIDs := []string{}
+
+ 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").
+ // Select only subs of given perm type.
+ Where(
+ "? = ?",
+ bun.Ident("domain_permission_subscription.permission_type"),
+ permType,
+ ).
+ // Order by priority descending.
+ OrderExpr(
+ "? DESC",
+ bun.Ident("domain_permission_subscription.priority"),
+ )
+
+ 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
+ }
+
+ // 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) UpdateDomainPermissionSubscription(
+ ctx context.Context,
+ permSubscription *gtsmodel.DomainPermissionSubscription,
+ columns ...string,
+) error {
+ return d.state.Caches.DB.DomainPermissionSubscription.Store(
+ permSubscription,
+ func() error {
+ _, err := d.db.
+ NewUpdate().
+ Model(permSubscription).
+ Where("? = ?", bun.Ident("id"), permSubscription.ID).
+ Column(columns...).
+ 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) CountDomainPermissionSubscriptionPerms(
+ ctx context.Context,
+ id string,
+) (int, error) {
+ permSubscription, err := d.GetDomainPermissionSubscriptionByID(
+ gtscontext.SetBarebones(ctx),
+ id,
+ )
+ if err != nil {
+ return 0, err
+ }
+
+ q := d.db.NewSelect()
+
+ if permSubscription.PermissionType == gtsmodel.DomainPermissionBlock {
+ q = q.TableExpr(
+ "? AS ?",
+ bun.Ident("domain_blocks"),
+ bun.Ident("perm"),
+ )
+ } else {
+ q = q.TableExpr(
+ "? AS ?",
+ bun.Ident("domain_allows"),
+ bun.Ident("perm"),
+ )
+ }
+
+ return q.
+ Column("perm.id").
+ Where("? = ?", bun.Ident("perm.subscription_id"), id).
+ Count(ctx)
+}
diff --git a/internal/db/bundb/domainpermissionsubscription_test.go b/internal/db/bundb/domainpermissionsubscription_test.go
new file mode 100644
index 000000000..732befbff
--- /dev/null
+++ b/internal/db/bundb/domainpermissionsubscription_test.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 <http://www.gnu.org/licenses/>.
+
+package bundb_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+type DomainPermissionSubscriptionTestSuite struct {
+ BunDBStandardTestSuite
+}
+
+func (suite *DomainPermissionSubscriptionTestSuite) TestCount() {
+ var (
+ ctx = context.Background()
+ testAccount = suite.testAccounts["admin_account"]
+ permSub = &gtsmodel.DomainPermissionSubscription{
+ ID: "01JGV3VZ72K58BYW8H5GEVBZGN",
+ PermissionType: gtsmodel.DomainPermissionBlock,
+ CreatedByAccountID: testAccount.ID,
+ CreatedByAccount: testAccount,
+ URI: "https://example.org/whatever.csv",
+ ContentType: gtsmodel.DomainPermSubContentTypeCSV,
+ }
+ perms = []*gtsmodel.DomainBlock{
+ {
+ ID: "01JGV42G72YCKN06AC51RZPFES",
+ Domain: "whatever.com",
+ CreatedByAccountID: testAccount.ID,
+ CreatedByAccount: testAccount,
+ SubscriptionID: permSub.ID,
+ },
+ {
+ ID: "01JGV43ZQKYPHM2M0YBQDFDSD1",
+ Domain: "aaaa.example.org",
+ CreatedByAccountID: testAccount.ID,
+ CreatedByAccount: testAccount,
+ SubscriptionID: permSub.ID,
+ },
+ {
+ ID: "01JGV444KDDC4WFG6MZQVM0N2Z",
+ Domain: "bbbb.example.org",
+ CreatedByAccountID: testAccount.ID,
+ CreatedByAccount: testAccount,
+ SubscriptionID: permSub.ID,
+ },
+ {
+ ID: "01JGV44AFEMBWS6P6S72BQK376",
+ Domain: "cccc.example.org",
+ CreatedByAccountID: testAccount.ID,
+ CreatedByAccount: testAccount,
+ SubscriptionID: permSub.ID,
+ },
+ }
+ )
+
+ // Whack the perm sub in the DB.
+ if err := suite.state.DB.PutDomainPermissionSubscription(ctx, permSub); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Whack the perms in the db.
+ for _, perm := range perms {
+ if err := suite.state.DB.CreateDomainBlock(ctx, perm); err != nil {
+ suite.FailNow(err.Error())
+ }
+ }
+
+ // Count 'em.
+ count, err := suite.state.DB.CountDomainPermissionSubscriptionPerms(ctx, permSub.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(4, count)
+}
+
+func TestDomainPermissionSubscriptionTestSuite(t *testing.T) {
+ suite.Run(t, new(DomainPermissionSubscriptionTestSuite))
+}
diff --git a/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude.go b/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude.go
index e19ea2b4d..32485ec64 100644
--- a/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude.go
+++ b/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude.go
@@ -20,7 +20,7 @@ package migrations
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude"
"github.com/uptrace/bun"
)
diff --git a/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissiondraft.go b/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissiondraft.go
new file mode 100644
index 000000000..e93b86f5c
--- /dev/null
+++ b/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissiondraft.go
@@ -0,0 +1,33 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package gtsmodel
+
+import "time"
+
+type DomainPermissionDraft struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ PermissionType uint8 `bun:",notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
+ Domain string `bun:",nullzero,notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
+ CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
+ PrivateComment string `bun:",nullzero"`
+ PublicComment string `bun:",nullzero"`
+ Obfuscate *bool `bun:",nullzero,notnull,default:false"`
+ SubscriptionID string `bun:"type:CHAR(26),unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
+}
diff --git a/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissionexclude.go b/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissionexclude.go
new file mode 100644
index 000000000..3ff46ba23
--- /dev/null
+++ b/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude/domainpermissionexclude.go
@@ -0,0 +1,31 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package gtsmodel
+
+import (
+ "time"
+)
+
+type DomainPermissionExclude struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
+ Domain string `bun:",nullzero,notnull,unique"`
+ CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
+ PrivateComment string `bun:",nullzero"`
+}
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..7d2bd085c
--- /dev/null
+++ b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go
@@ -0,0 +1,75 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package migrations
+
+import (
+ "context"
+
+ gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions"
+ "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_subscriptions`.
+ if _, err := tx.
+ NewCreateTable().
+ Model((*gtsmodel.DomainPermissionSubscription)(nil)).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Create indexes. Indices. Indie sexes.
+ if _, err := tx.
+ NewCreateIndex().
+ Table("domain_permission_subscriptions").
+ // Filter on permission type.
+ Index("domain_permission_subscriptions_permission_type_idx").
+ Column("permission_type").
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ if _, err := tx.
+ NewCreateIndex().
+ Table("domain_permission_subscriptions").
+ // Sort by priority DESC.
+ Index("domain_permission_subscriptions_priority_order_idx").
+ ColumnExpr("? DESC", bun.Ident("priority")).
+ 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/bundb/migrations/20241022153016_domain_permission_subscriptions/domainpermissionsubscription.go b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions/domainpermissionsubscription.go
new file mode 100644
index 000000000..851f44f15
--- /dev/null
+++ b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions/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 <http://www.gnu.org/licenses/>.
+
+package gtsmodel
+
+import "time"
+
+type DomainPermissionSubscription struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
+ Priority uint8 `bun:""`
+ Title string `bun:",nullzero,unique"`
+ PermissionType uint8 `bun:",nullzero,notnull"`
+ AsDraft *bool `bun:",nullzero,notnull,default:true"`
+ AdoptOrphans *bool `bun:",nullzero,notnull,default:false"`
+ CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
+ URI string `bun:",nullzero,notnull,unique"`
+ ContentType uint16 `bun:",nullzero,notnull"`
+ FetchUsername string `bun:",nullzero"`
+ FetchPassword string `bun:",nullzero"`
+ FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
+ SuccessfullyFetchedAt time.Time `bun:"type:timestamptz,nullzero"`
+ ETag string `bun:"etag,nullzero"`
+ Error string `bun:",nullzero"`
+}
diff --git a/internal/db/domain.go b/internal/db/domain.go
index f4d05ad1d..643538e7e 100644
--- a/internal/db/domain.go
+++ b/internal/db/domain.go
@@ -132,4 +132,44 @@ type Domain interface {
// IsDomainPermissionExcluded returns true if the given domain matches in the list of excluded domains.
IsDomainPermissionExcluded(ctx context.Context, domain string) (bool, 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)
+
+ // GetDomainPermissionSubscriptionsByPriority returns *all* domain permission
+ // subscriptions of the given permission type, sorted by priority descending.
+ GetDomainPermissionSubscriptionsByPriority(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+ ) ([]*gtsmodel.DomainPermissionSubscription, error)
+
+ // PutDomainPermissionSubscription stores one DomainPermissionSubscription.
+ PutDomainPermissionSubscription(ctx context.Context, permSub *gtsmodel.DomainPermissionSubscription) error
+
+ // UpdateDomainPermissionSubscription updates the provided
+ // columns of one DomainPermissionSubscription.
+ UpdateDomainPermissionSubscription(
+ ctx context.Context,
+ permSub *gtsmodel.DomainPermissionSubscription,
+ columns ...string,
+ ) error
+
+ // DeleteDomainPermissionSubscription deletes one DomainPermissionSubscription with the given id.
+ DeleteDomainPermissionSubscription(ctx context.Context, id string) error
+
+ // CountDomainPermissionSubscriptionPerms counts the number of permissions
+ // currently managed by the domain permission subscription of the given ID.
+ CountDomainPermissionSubscriptionPerms(ctx context.Context, id string) (int, error)
}
diff --git a/internal/gtsmodel/domainpermissionsubscription.go b/internal/gtsmodel/domainpermissionsubscription.go
new file mode 100644
index 000000000..b6a0b8f43
--- /dev/null
+++ b/internal/gtsmodel/domainpermissionsubscription.go
@@ -0,0 +1,74 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package gtsmodel
+
+import "time"
+
+type DomainPermissionSubscription struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
+ Priority uint8 `bun:""` // Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
+ Title string `bun:",nullzero,unique"` // Moderator-set title for this list.
+ PermissionType DomainPermissionType `bun:",nullzero,notnull"` // Permission type of the subscription.
+ AsDraft *bool `bun:",nullzero,notnull,default:true"` // Create domain permission entries resulting from this subscription as drafts.
+ AdoptOrphans *bool `bun:",nullzero,notnull,default:false"` // Adopt orphaned domain permissions present in this subscription's entries.
+ CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this subscription.
+ CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID.
+ URI string `bun:",nullzero,notnull,unique"` // URI of the domain permission list.
+ ContentType DomainPermSubContentType `bun:",nullzero,notnull"` // Content type to expect from the URI.
+ 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.
+ SuccessfullyFetchedAt time.Time `bun:"type:timestamptz,nullzero"` // Time when the domain permission list was last *successfuly* fetched, to be transmitted as If-Modified-Since header.
+ ETag string `bun:"etag,nullzero"` // Etag last received from the server (if any) on successful fetch.
+ Error string `bun:",nullzero"` // If latest fetch attempt errored, this field stores the error message. Cleared on latest successful fetch.
+}
+
+type DomainPermSubContentType enumType
+
+const (
+ DomainPermSubContentTypeUnknown DomainPermSubContentType = 0 // ???
+ DomainPermSubContentTypeCSV DomainPermSubContentType = 1 // text/csv
+ DomainPermSubContentTypeJSON DomainPermSubContentType = 2 // application/json
+ DomainPermSubContentTypePlain DomainPermSubContentType = 3 // text/plain
+)
+
+func (p DomainPermSubContentType) String() string {
+ switch p {
+ case DomainPermSubContentTypeCSV:
+ return "text/csv"
+ case DomainPermSubContentTypeJSON:
+ return "application/json"
+ case DomainPermSubContentTypePlain:
+ return "text/plain"
+ default:
+ panic("unknown content type")
+ }
+}
+
+func NewDomainPermSubContentType(in string) DomainPermSubContentType {
+ switch in {
+ case "text/csv":
+ return DomainPermSubContentTypeCSV
+ case "application/json":
+ return DomainPermSubContentTypeJSON
+ case "text/plain":
+ return DomainPermSubContentTypePlain
+ default:
+ return DomainPermSubContentTypeUnknown
+ }
+}
diff --git a/internal/id/ulid.go b/internal/id/ulid.go
index 8c0b1e94c..3c57c9f1b 100644
--- a/internal/id/ulid.go
+++ b/internal/id/ulid.go
@@ -83,3 +83,12 @@ func NewRandomULID() (string, error) {
}
return newUlid.String(), nil
}
+
+func TimeFromULID(id string) (time.Time, error) {
+ parsed, err := ulid.ParseStrict(id)
+ if err != nil {
+ return time.Time{}, err
+ }
+
+ return ulid.Time(parsed.Time()), nil
+}
diff --git a/internal/processing/admin/domainpermissionsubscription.go b/internal/processing/admin/domainpermissionsubscription.go
new file mode 100644
index 000000000..3d2f63d56
--- /dev/null
+++ b/internal/processing/admin/domainpermissionsubscription.go
@@ -0,0 +1,285 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package admin
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+)
+
+// DomainPermissionSubscriptionGet returns one
+// domain permission subscription with the given id.
+func (p *Processor) DomainPermissionSubscriptionGet(
+ ctx context.Context,
+ id string,
+) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ permSub, err := p.state.DB.GetDomainPermissionSubscriptionByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting domain permission subscription %s: %w", id, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if permSub == nil {
+ err := fmt.Errorf("domain permission subscription %s not found", id)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ return p.apiDomainPermSub(ctx, permSub)
+}
+
+// DomainPermissionSubscriptionsGet returns a page of
+// DomainPermissionSubscriptions with the given parameters.
+func (p *Processor) DomainPermissionSubscriptionsGet(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+ page *paging.Page,
+) (*apimodel.PageableResponse, gtserror.WithCode) {
+ permSubs, err := p.state.DB.GetDomainPermissionSubscriptions(
+ ctx,
+ permType,
+ page,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ count := len(permSubs)
+ if count == 0 {
+ return paging.EmptyResponse(), nil
+ }
+
+ // Get the lowest and highest
+ // ID values, used for paging.
+ lo := permSubs[count-1].ID
+ hi := permSubs[0].ID
+
+ // Convert each perm sub to API model.
+ items := make([]any, len(permSubs))
+ for i, permSub := range permSubs {
+ apiPermSub, err := p.converter.DomainPermSubToAPIDomainPermSub(ctx, permSub)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ items[i] = apiPermSub
+ }
+
+ // Assemble next/prev page queries.
+ query := make(url.Values, 1)
+ if permType != gtsmodel.DomainPermissionUnknown {
+ query.Set(apiutil.DomainPermissionPermTypeKey, permType.String())
+ }
+
+ return paging.PackageResponse(paging.ResponseParams{
+ Items: items,
+ Path: "/api/v1/admin/domain_permission_subscriptions",
+ Next: page.Next(lo, hi),
+ Prev: page.Prev(lo, hi),
+ Query: query,
+ }), nil
+}
+
+// DomainPermissionSubscriptionsGetByPriority returns all domain permission
+// subscriptions of the given permission type, in descending priority order.
+func (p *Processor) DomainPermissionSubscriptionsGetByPriority(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+) ([]*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ permSubs, err := p.state.DB.GetDomainPermissionSubscriptionsByPriority(
+ ctx,
+ permType,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Convert each perm sub to API model.
+ items := make([]*apimodel.DomainPermissionSubscription, len(permSubs))
+ for i, permSub := range permSubs {
+ apiPermSub, err := p.converter.DomainPermSubToAPIDomainPermSub(ctx, permSub)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ items[i] = apiPermSub
+ }
+
+ return items, nil
+}
+
+func (p *Processor) DomainPermissionSubscriptionCreate(
+ ctx context.Context,
+ acct *gtsmodel.Account,
+ priority uint8,
+ title string,
+ uri string,
+ contentType gtsmodel.DomainPermSubContentType,
+ permType gtsmodel.DomainPermissionType,
+ asDraft bool,
+ fetchUsername string,
+ fetchPassword string,
+) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ permSub := &gtsmodel.DomainPermissionSubscription{
+ ID: id.NewULID(),
+ Priority: priority,
+ Title: title,
+ PermissionType: permType,
+ AsDraft: &asDraft,
+ CreatedByAccountID: acct.ID,
+ CreatedByAccount: acct,
+ URI: uri,
+ ContentType: contentType,
+ FetchUsername: fetchUsername,
+ FetchPassword: fetchPassword,
+ }
+
+ err := p.state.DB.PutDomainPermissionSubscription(ctx, permSub)
+ if err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ // Unique constraint conflict.
+ const errText = "domain permission subscription with given URI or title already exists"
+ return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
+ }
+
+ // Real database error.
+ err := gtserror.Newf("db error putting domain permission subscription: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.apiDomainPermSub(ctx, permSub)
+}
+
+func (p *Processor) DomainPermissionSubscriptionUpdate(
+ ctx context.Context,
+ id string,
+ priority *uint8,
+ title *string,
+ uri *string,
+ contentType *gtsmodel.DomainPermSubContentType,
+ asDraft *bool,
+ adoptOrphans *bool,
+ fetchUsername *string,
+ fetchPassword *string,
+) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ permSub, err := p.state.DB.GetDomainPermissionSubscriptionByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting domain permission subscription %s: %w", id, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if permSub == nil {
+ err := fmt.Errorf("domain permission subscription %s not found", id)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ columns := make([]string, 0, 7)
+
+ if priority != nil {
+ permSub.Priority = *priority
+ columns = append(columns, "priority")
+ }
+
+ if title != nil {
+ permSub.Title = *title
+ columns = append(columns, "title")
+ }
+
+ if uri != nil {
+ permSub.URI = *uri
+ columns = append(columns, "uri")
+ }
+
+ if contentType != nil {
+ permSub.ContentType = *contentType
+ columns = append(columns, "content_type")
+ }
+
+ if asDraft != nil {
+ permSub.AsDraft = asDraft
+ columns = append(columns, "as_draft")
+ }
+
+ if adoptOrphans != nil {
+ permSub.AdoptOrphans = adoptOrphans
+ columns = append(columns, "adopt_orphans")
+ }
+
+ if fetchPassword != nil {
+ permSub.FetchPassword = *fetchPassword
+ columns = append(columns, "fetch_password")
+ }
+
+ if fetchUsername != nil {
+ permSub.FetchUsername = *fetchUsername
+ columns = append(columns, "fetch_username")
+ }
+
+ err = p.state.DB.UpdateDomainPermissionSubscription(ctx, permSub, columns...)
+ if err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ // Unique constraint conflict.
+ const errText = "domain permission subscription with given URI or title already exists"
+ return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
+ }
+
+ // Real database error.
+ err := gtserror.Newf("db error updating domain permission subscription: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.apiDomainPermSub(ctx, permSub)
+}
+
+func (p *Processor) DomainPermissionSubscriptionRemove(
+ ctx context.Context,
+ id string,
+ removeChildren bool,
+) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ permSub, err := p.state.DB.GetDomainPermissionSubscriptionByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting domain permission subscription %s: %w", id, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if permSub == nil {
+ err := fmt.Errorf("domain permission subscription %s not found", id)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ // TODO in next PR: if removeChildren, then remove all
+ // domain permissions that are children of this domain
+ // permission subscription. If not removeChildren, then
+ // just unlink them by clearing their subscription ID.
+ // For now just delete the domain permission subscription.
+ if err := p.state.DB.DeleteDomainPermissionSubscription(ctx, id); err != nil {
+ err := gtserror.Newf("db error deleting domain permission subscription: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.apiDomainPermSub(ctx, permSub)
+}
diff --git a/internal/processing/admin/util.go b/internal/processing/admin/util.go
index bc59a2b3b..aef435856 100644
--- a/internal/processing/admin/util.go
+++ b/internal/processing/admin/util.go
@@ -115,3 +115,19 @@ func (p *Processor) apiDomainPerm(
return apiDomainPerm, nil
}
+
+// apiDomainPermSub is a cheeky shortcut for returning the
+// API version of the given domain permission subscription,
+// or an appropriate error if something goes wrong.
+func (p *Processor) apiDomainPermSub(
+ ctx context.Context,
+ domainPermSub *gtsmodel.DomainPermissionSubscription,
+) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ apiDomainPermSub, err := p.converter.DomainPermSubToAPIDomainPermSub(ctx, domainPermSub)
+ if err != nil {
+ err := gtserror.NewfAt(3, "error converting domain permission subscription to api model: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiDomainPermSub, nil
+}
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 0d5e15078..9fb69b438 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/language"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
@@ -2130,6 +2131,60 @@ func (c *Converter) DomainPermToAPIDomainPerm(
return domainPerm, nil
}
+func (c *Converter) DomainPermSubToAPIDomainPermSub(
+ ctx context.Context,
+ d *gtsmodel.DomainPermissionSubscription,
+) (*apimodel.DomainPermissionSubscription, error) {
+ createdAt, err := id.TimeFromULID(d.ID)
+ if err != nil {
+ return nil, gtserror.Newf("error converting id to time: %w", err)
+ }
+
+ // URI may be in Punycode,
+ // de-punify it just in case.
+ uri, err := util.DePunify(d.URI)
+ if err != nil {
+ return nil, gtserror.Newf("error de-punifying URI %s: %w", d.URI, err)
+ }
+
+ var (
+ fetchedAt string
+ successfullyFetchedAt string
+ )
+
+ if !d.FetchedAt.IsZero() {
+ fetchedAt = util.FormatISO8601(d.FetchedAt)
+ }
+
+ if !d.SuccessfullyFetchedAt.IsZero() {
+ successfullyFetchedAt = util.FormatISO8601(d.SuccessfullyFetchedAt)
+ }
+
+ count, err := c.state.DB.CountDomainPermissionSubscriptionPerms(ctx, d.ID)
+ if err != nil {
+ return nil, gtserror.Newf("error counting perm sub perms: %w", err)
+ }
+
+ return &apimodel.DomainPermissionSubscription{
+ ID: d.ID,
+ Priority: d.Priority,
+ Title: d.Title,
+ PermissionType: d.PermissionType.String(),
+ AsDraft: *d.AsDraft,
+ AdoptOrphans: *d.AdoptOrphans,
+ CreatedBy: d.CreatedByAccountID,
+ CreatedAt: util.FormatISO8601(createdAt),
+ URI: uri,
+ ContentType: d.ContentType.String(),
+ FetchUsername: d.FetchUsername,
+ FetchPassword: d.FetchPassword,
+ FetchedAt: fetchedAt,
+ SuccessfullyFetchedAt: successfullyFetchedAt,
+ Error: d.Error,
+ Count: uint64(count), // #nosec G115 -- Don't care about overflow here.
+ }, nil
+}
+
// ReportToAPIReport converts a gts model report into an api model report, for serving at /api/v1/reports
func (c *Converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (*apimodel.Report, error) {
report := &apimodel.Report{