diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
index 5c0c2ae3d..0f04b095a 100644
--- a/docs/api/swagger.yaml
+++ b/docs/api/swagger.yaml
@@ -1130,6 +1130,78 @@ definitions:
type: object
x-go-name: DomainPermission
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ domainPermissionSubscription:
+ properties:
+ as_draft:
+ description: If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. If false, domain permissions from this subscription will come into force immediately.
+ example: true
+ type: boolean
+ x-go-name: AsDraft
+ content_type:
+ description: MIME content type to use when parsing the permissions list.
+ example: text/csv
+ type: string
+ x-go-name: ContentType
+ count:
+ description: Count of domain permission entries discovered at URI on last (successful) fetch.
+ example: 53
+ format: uint64
+ readOnly: true
+ type: integer
+ x-go-name: Count
+ created_by_account_id:
+ description: ID of the account that created this subscription.
+ example: 01FBW21XJA09XYX51KV5JVBW0F
+ readOnly: true
+ type: string
+ x-go-name: CreatedByAccountID
+ error:
+ description: If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
+ example: Oopsie doopsie, we made a fucky wucky.
+ readOnly: true
+ type: string
+ x-go-name: Error
+ fetch_password:
+ description: (Optional) password to set for basic auth when doing a fetch of URI.
+ example: admin123
+ type: string
+ x-go-name: FetchPassword
+ fetch_username:
+ description: (Optional) username to set for basic auth when doing a fetch of URI.
+ example: admin123
+ type: string
+ x-go-name: FetchUsername
+ fetched_at:
+ description: Time at which the most recent fetch was attempted (ISO 8601 Datetime).
+ example: "2021-07-30T09:20:25+00:00"
+ readOnly: true
+ type: string
+ x-go-name: FetchedAt
+ id:
+ description: The ID of the domain permission subscription.
+ example: 01FBW21XJA09XYX51KV5JVBW0F
+ readOnly: true
+ type: string
+ x-go-name: ID
+ permission_type:
+ description: The type of domain permission subscription (allow, block).
+ example: block
+ type: string
+ x-go-name: PermissionType
+ title:
+ description: Title of this list, as set by admin who created or updated it.f
+ example: really cool list of neato pals
+ type: string
+ x-go-name: Title
+ uri:
+ description: URI to call in order to fetch the permissions list.
+ example: https://www.example.org/blocklists/list1.csv
+ type: string
+ x-go-name: URI
+ title: DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
+ type: object
+ x-go-name: DomainPermissionSubscription
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
emoji:
properties:
category:
@@ -5997,6 +6069,70 @@ paths:
summary: Get domain permission exclude with the given ID.
tags:
- admin
+ /api/v1/admin/domain_permission_subscriptions:
+ post:
+ consumes:
+ - multipart/form-data
+ - application/json
+ operationId: domainPermissionSubscriptionCreate
+ parameters:
+ - description: Optional title for this subscription.
+ in: formData
+ name: title
+ type: string
+ - description: Type of permissions to create by parsing the targeted file/list. One of "allow" or "block".
+ in: formData
+ name: permission_type
+ required: true
+ type: string
+ - default: true
+ 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".
+ in: formData
+ name: as_draft
+ type: boolean
+ - description: URI to call in order to fetch the permissions list.
+ in: formData
+ name: uri
+ required: true
+ type: string
+ - description: MIME content type to use when parsing the permissions list. One of "text/plain", "text/csv", and "application/json".
+ in: formData
+ name: content_type
+ required: true
+ type: string
+ - description: Optional basic auth username to provide when fetching given uri. If set, will be transmitted along with `fetch_password` when doing the fetch.
+ in: formData
+ name: fetch_username
+ type: string
+ - description: Optional basic auth password to provide when fetching given uri. If set, will be transmitted along with `fetch_username` when doing the fetch.
+ in: formData
+ name: fetch_password
+ type: string
+ produces:
+ - application/json
+ 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
+ security:
+ - OAuth2 Bearer:
+ - admin
+ summary: Create a domain permission subscription with the given parameters.
+ tags:
+ - admin
/api/v1/admin/email/test:
post:
consumes:
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go
index a33a6448a..9da6df504 100644
--- a/internal/api/client/admin/admin.go
+++ b/internal/api/client/admin/admin.go
@@ -28,43 +28,45 @@ 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
+ 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 +120,11 @@ 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, DomainPermissionSubscriptionsPathWithID, m.DomainPermissionSubscriptionGETHandler)
+
// 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/domainpermissionsubscriptioncreate.go b/internal/api/client/admin/domainpermissionsubscriptioncreate.go
new file mode 100644
index 000000000..b6cbdff01
--- /dev/null
+++ b/internal/api/client/admin/domainpermissionsubscriptioncreate.go
@@ -0,0 +1,207 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "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: 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: 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
+ }
+
+ // Ensure URI is set.
+ if form.URI == "" {
+ 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.
+ contentType, errWithCode := parseDomainPermSubContentType(form.ContentType)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Permission type must be set.
+ permType, errWithCode := parseDomainPermissionType(form.PermissionType)
+ 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,
+ form.Title,
+ uriStr,
+ contentType,
+ permType,
+ asDraft,
+ 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/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 .
+
+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/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 .
+
+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:
+//
+// ```
+// ; rel="next", ; 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/model/domain.go b/internal/api/model/domain.go
index c973c7d92..4a95af48a 100644
--- a/internal/api/model/domain.go
+++ b/internal/api/model/domain.go
@@ -99,3 +99,84 @@ 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"`
+ // 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"`
+ // 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 {
+ // 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"`
+ // (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 a4f9f2044..31c4301da 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 aac11236a..b0bbe0eaa 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
@@ -586,6 +589,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 26f4096ed..5a7688223 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -357,6 +357,20 @@ func sizeofDomainPermissionDraft() uintptr {
}))
}
+func sizeofDomainPermissionSubscription() uintptr {
+ return uintptr(size.Of(>smodel.DomainPermissionSubscription{
+ ID: exampleID,
+ CreatedAt: exampleTime,
+ PermissionType: gtsmodel.DomainPermissionBlock,
+ CreatedByAccountID: exampleID,
+ URI: exampleURI,
+ FetchUsername: "username",
+ FetchPassword: "password",
+ FetchedAt: exampleTime,
+ AsDraft: util.Ptr(true),
+ }))
+}
+
func sizeofEmoji() uintptr {
return uintptr(size.Of(>smodel.Emoji{
ID: exampleID,
diff --git a/internal/config/config.go b/internal/config/config.go
index 2e3ad8ec1..661744929 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -196,59 +196,60 @@ type HTTPClientConfiguration struct {
}
type CacheConfiguration struct {
- MemoryTarget bytesize.Size `name:"memory-target"`
- AccountMemRatio float64 `name:"account-mem-ratio"`
- AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
- AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"`
- AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"`
- ApplicationMemRatio float64 `name:"application-mem-ratio"`
- BlockMemRatio float64 `name:"block-mem-ratio"`
- BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"`
- BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"`
- ClientMemRatio float64 `name:"client-mem-ratio"`
- ConversationMemRatio float64 `name:"conversation-mem-ratio"`
- ConversationLastStatusIDsMemRatio float64 `name:"conversation-last-status-ids-mem-ratio"`
- 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"`
- StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
- StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
- TagMemRatio float64 `name:"tag-mem-ratio"`
- ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"`
- TokenMemRatio float64 `name:"token-mem-ratio"`
- TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
- UserMemRatio float64 `name:"user-mem-ratio"`
- UserMuteMemRatio float64 `name:"user-mute-mem-ratio"`
- UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"`
- WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
- VisibilityMemRatio float64 `name:"visibility-mem-ratio"`
+ MemoryTarget bytesize.Size `name:"memory-target"`
+ AccountMemRatio float64 `name:"account-mem-ratio"`
+ AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
+ AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"`
+ AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"`
+ ApplicationMemRatio float64 `name:"application-mem-ratio"`
+ BlockMemRatio float64 `name:"block-mem-ratio"`
+ BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"`
+ BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"`
+ ClientMemRatio float64 `name:"client-mem-ratio"`
+ ConversationMemRatio float64 `name:"conversation-mem-ratio"`
+ ConversationLastStatusIDsMemRatio float64 `name:"conversation-last-status-ids-mem-ratio"`
+ DomainPermissionDraftMemRation float64 `name:"domain-permission-draft-mem-ratio"`
+ DomainPermissionSubscriptionMemRation float64 `name:"domain-permission-subscription-mem-ratio"`
+ EmojiMemRatio float64 `name:"emoji-mem-ratio"`
+ EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"`
+ FilterMemRatio float64 `name:"filter-mem-ratio"`
+ FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"`
+ FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"`
+ FollowMemRatio float64 `name:"follow-mem-ratio"`
+ FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"`
+ FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"`
+ FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
+ FollowingTagIDsMemRatio float64 `name:"following-tag-ids-mem-ratio"`
+ InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
+ InstanceMemRatio float64 `name:"instance-mem-ratio"`
+ InteractionRequestMemRatio float64 `name:"interaction-request-mem-ratio"`
+ ListMemRatio float64 `name:"list-mem-ratio"`
+ ListIDsMemRatio float64 `name:"list-ids-mem-ratio"`
+ ListedIDsMemRatio float64 `name:"listed-ids-mem-ratio"`
+ MarkerMemRatio float64 `name:"marker-mem-ratio"`
+ MediaMemRatio float64 `name:"media-mem-ratio"`
+ MentionMemRatio float64 `name:"mention-mem-ratio"`
+ MoveMemRatio float64 `name:"move-mem-ratio"`
+ NotificationMemRatio float64 `name:"notification-mem-ratio"`
+ PollMemRatio float64 `name:"poll-mem-ratio"`
+ PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"`
+ PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"`
+ ReportMemRatio float64 `name:"report-mem-ratio"`
+ SinBinStatusMemRatio float64 `name:"sin-bin-status-mem-ratio"`
+ StatusMemRatio float64 `name:"status-mem-ratio"`
+ StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"`
+ StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"`
+ StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
+ StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
+ TagMemRatio float64 `name:"tag-mem-ratio"`
+ ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"`
+ TokenMemRatio float64 `name:"token-mem-ratio"`
+ TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
+ UserMemRatio float64 `name:"user-mem-ratio"`
+ UserMuteMemRatio float64 `name:"user-mute-mem-ratio"`
+ UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"`
+ WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
+ VisibilityMemRatio float64 `name:"visibility-mem-ratio"`
}
// MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML).
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 9b45002d0..827822635 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -158,58 +158,59 @@ 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,
- StatusFaveMemRatio: 2,
- StatusFaveIDsMemRatio: 3,
- TagMemRatio: 2,
- ThreadMuteMemRatio: 0.2,
- TokenMemRatio: 0.75,
- TombstoneMemRatio: 0.5,
- UserMemRatio: 0.25,
- UserMuteMemRatio: 2,
- UserMuteIDsMemRatio: 3,
- WebfingerMemRatio: 0.1,
- VisibilityMemRatio: 2,
+ AccountMemRatio: 5,
+ AccountNoteMemRatio: 1,
+ AccountSettingsMemRatio: 0.1,
+ AccountStatsMemRatio: 2,
+ ApplicationMemRatio: 0.1,
+ BlockMemRatio: 2,
+ BlockIDsMemRatio: 3,
+ BoostOfIDsMemRatio: 3,
+ ClientMemRatio: 0.1,
+ ConversationMemRatio: 1,
+ ConversationLastStatusIDsMemRatio: 2,
+ DomainPermissionDraftMemRation: 0.5,
+ DomainPermissionSubscriptionMemRation: 0.5,
+ EmojiMemRatio: 3,
+ EmojiCategoryMemRatio: 0.1,
+ FilterMemRatio: 0.5,
+ FilterKeywordMemRatio: 0.5,
+ FilterStatusMemRatio: 0.5,
+ FollowMemRatio: 2,
+ FollowIDsMemRatio: 4,
+ FollowRequestMemRatio: 2,
+ FollowRequestIDsMemRatio: 2,
+ FollowingTagIDsMemRatio: 2,
+ InReplyToIDsMemRatio: 3,
+ InstanceMemRatio: 1,
+ InteractionRequestMemRatio: 1,
+ ListMemRatio: 1,
+ ListIDsMemRatio: 2,
+ ListedIDsMemRatio: 2,
+ MarkerMemRatio: 0.5,
+ MediaMemRatio: 4,
+ MentionMemRatio: 2,
+ MoveMemRatio: 0.1,
+ NotificationMemRatio: 2,
+ PollMemRatio: 1,
+ PollVoteMemRatio: 2,
+ PollVoteIDsMemRatio: 2,
+ ReportMemRatio: 1,
+ SinBinStatusMemRatio: 0.5,
+ StatusMemRatio: 5,
+ StatusBookmarkMemRatio: 0.5,
+ StatusBookmarkIDsMemRatio: 2,
+ StatusFaveMemRatio: 2,
+ StatusFaveIDsMemRatio: 3,
+ TagMemRatio: 2,
+ ThreadMuteMemRatio: 0.2,
+ TokenMemRatio: 0.75,
+ TombstoneMemRatio: 0.5,
+ UserMemRatio: 0.25,
+ UserMuteMemRatio: 2,
+ UserMuteIDsMemRatio: 3,
+ WebfingerMemRatio: 0.1,
+ VisibilityMemRatio: 2,
},
HTTPClient: HTTPClientConfiguration{
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index a35622f8e..e9330573b 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..0e4a95abf
--- /dev/null
+++ b/internal/db/bundb/domainpermissionsubscription.go
@@ -0,0 +1,246 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package bundb
+
+import (
+ "context"
+ "errors"
+ "slices"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+ "github.com/uptrace/bun"
+)
+
+func (d *domainDB) getDomainPermissionSubscription(
+ ctx context.Context,
+ lookup string,
+ dbQuery func(*gtsmodel.DomainPermissionSubscription) error,
+ keyParts ...any,
+) (*gtsmodel.DomainPermissionSubscription, error) {
+ // Fetch perm subscription from database cache with loader callback.
+ permSub, err := d.state.Caches.DB.DomainPermissionSubscription.LoadOne(
+ lookup,
+ // Only called if not cached.
+ func() (*gtsmodel.DomainPermissionSubscription, error) {
+ var permSub gtsmodel.DomainPermissionSubscription
+ if err := dbQuery(&permSub); err != nil {
+ return nil, err
+ }
+ return &permSub, nil
+ },
+ keyParts...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ if gtscontext.Barebones(ctx) {
+ // No need to fully populate.
+ return permSub, nil
+ }
+
+ if permSub.CreatedByAccount == nil {
+ // Not set, fetch from database.
+ permSub.CreatedByAccount, err = d.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ permSub.CreatedByAccountID,
+ )
+ if err != nil {
+ return nil, gtserror.Newf("error populating created by account: %w", err)
+ }
+ }
+
+ return permSub, nil
+}
+
+func (d *domainDB) GetDomainPermissionSubscriptionByID(
+ ctx context.Context,
+ id string,
+) (*gtsmodel.DomainPermissionSubscription, error) {
+ return d.getDomainPermissionSubscription(
+ ctx,
+ "ID",
+ func(permSub *gtsmodel.DomainPermissionSubscription) error {
+ return d.db.
+ NewSelect().
+ Model(permSub).
+ Where("? = ?", bun.Ident("domain_permission_subscription.id"), id).
+ Scan(ctx)
+ },
+ id,
+ )
+}
+
+func (d *domainDB) GetDomainPermissionSubscriptions(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+ page *paging.Page,
+) (
+ []*gtsmodel.DomainPermissionSubscription,
+ error,
+) {
+ var (
+ // Get paging params.
+ minID = page.GetMin()
+ maxID = page.GetMax()
+ limit = page.GetLimit()
+ order = page.GetOrder()
+
+ // Make educated guess for slice size
+ permSubIDs = make([]string, 0, limit)
+ )
+
+ q := d.db.
+ NewSelect().
+ TableExpr(
+ "? AS ?",
+ bun.Ident("domain_permission_subscriptions"),
+ bun.Ident("domain_permission_subscription"),
+ ).
+ // Select only IDs from table
+ Column("domain_permission_subscription.id")
+
+ // Return only items with id
+ // lower than provided maxID.
+ if maxID != "" {
+ q = q.Where(
+ "? < ?",
+ bun.Ident("domain_permission_subscription.id"),
+ maxID,
+ )
+ }
+
+ // Return only items with id
+ // greater than provided minID.
+ if minID != "" {
+ q = q.Where(
+ "? > ?",
+ bun.Ident("domain_permission_subscription.id"),
+ minID,
+ )
+ }
+
+ // Return only items with
+ // given permission type.
+ if permType != gtsmodel.DomainPermissionUnknown {
+ q = q.Where(
+ "? = ?",
+ bun.Ident("domain_permission_subscription.permission_type"),
+ permType,
+ )
+ }
+
+ if limit > 0 {
+ // Limit amount of
+ // items returned.
+ q = q.Limit(limit)
+ }
+
+ if order == paging.OrderAscending {
+ // Page up.
+ q = q.OrderExpr(
+ "? ASC",
+ bun.Ident("domain_permission_subscription.id"),
+ )
+ } else {
+ // Page down.
+ q = q.OrderExpr(
+ "? DESC",
+ bun.Ident("domain_permission_subscription.id"),
+ )
+ }
+
+ if err := q.Scan(ctx, &permSubIDs); err != nil {
+ return nil, err
+ }
+
+ // Catch case of no items early
+ if len(permSubIDs) == 0 {
+ return nil, db.ErrNoEntries
+ }
+
+ // If we're paging up, we still want items
+ // to be sorted by ID desc, so reverse slice.
+ if order == paging.OrderAscending {
+ slices.Reverse(permSubIDs)
+ }
+
+ // Allocate return slice (will be at most len permSubIDs).
+ permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs))
+ for _, id := range permSubIDs {
+ permSub, err := d.GetDomainPermissionSubscriptionByID(ctx, id)
+ if err != nil {
+ log.Errorf(ctx, "error getting domain permission subscription %q: %v", id, err)
+ continue
+ }
+
+ // Append to return slice
+ permSubs = append(permSubs, permSub)
+ }
+
+ return permSubs, nil
+}
+
+func (d *domainDB) PutDomainPermissionSubscription(
+ ctx context.Context,
+ permSubscription *gtsmodel.DomainPermissionSubscription,
+) error {
+ return d.state.Caches.DB.DomainPermissionSubscription.Store(
+ permSubscription,
+ func() error {
+ _, err := d.db.
+ NewInsert().
+ Model(permSubscription).
+ Exec(ctx)
+ return err
+ },
+ )
+}
+
+func (d *domainDB) DeleteDomainPermissionSubscription(
+ ctx context.Context,
+ id string,
+) error {
+ // Delete the permSub from DB.
+ q := d.db.NewDelete().
+ TableExpr(
+ "? AS ?",
+ bun.Ident("domain_permission_subscriptions"),
+ bun.Ident("domain_permission_subscription"),
+ ).
+ Where(
+ "? = ?",
+ bun.Ident("domain_permission_subscription.id"),
+ id,
+ )
+
+ _, err := q.Exec(ctx)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return err
+ }
+
+ // Invalidate any cached model by ID.
+ d.state.Caches.DB.DomainPermissionSubscription.Invalidate("ID", id)
+
+ return nil
+}
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..e5f719cc2
--- /dev/null
+++ b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go
@@ -0,0 +1,71 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package migrations
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ // Create `domain_permission_subscriptions`.
+ if _, err := tx.
+ NewCreateTable().
+ Model((*gtsmodel.DomainPermissionSubscription)(nil)).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Create indexes. Indices. Indie sexes.
+ for table, indexes := range map[string]map[string][]string{
+ "domain_permission_subscriptions": {
+ "domain_permission_subscriptions_permission_type_idx": {"permission_type"},
+ },
+ } {
+ for index, columns := range indexes {
+ if _, err := tx.
+ NewCreateIndex().
+ Table(table).
+ Index(index).
+ Column(columns...).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/domain.go b/internal/db/domain.go
index f4d05ad1d..c5a9bea6d 100644
--- a/internal/db/domain.go
+++ b/internal/db/domain.go
@@ -132,4 +132,25 @@ 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)
+
+ // PutDomainPermissionSubscription stores one DomainPermissionSubscription.
+ PutDomainPermissionSubscription(ctx context.Context, permSub *gtsmodel.DomainPermissionSubscription) error
+
+ // DeleteDomainPermissionSubscription deletes one DomainPermissionSubscription with the given id.
+ DeleteDomainPermissionSubscription(ctx context.Context, id string) error
}
diff --git a/internal/gtsmodel/domainpermissionsubscription.go b/internal/gtsmodel/domainpermissionsubscription.go
new file mode 100644
index 000000000..31003c63b
--- /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 .
+
+package gtsmodel
+
+import "time"
+
+type DomainPermissionSubscription struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Time when this item was created.
+ Title string `bun:",nullzero,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.
+ 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:",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.
+ Count uint64 `bun:""` // Count of domain permission entries discovered at URI.
+}
+
+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 DomainPermSubContentTypeCSV
+ case "text/plain":
+ return DomainPermSubContentTypeCSV
+ default:
+ return DomainPermSubContentTypeUnknown
+ }
+}
diff --git a/internal/processing/admin/domainpermissionsubscription.go b/internal/processing/admin/domainpermissionsubscription.go
new file mode 100644
index 000000000..a8bf0b530
--- /dev/null
+++ b/internal/processing/admin/domainpermissionsubscription.go
@@ -0,0 +1,145 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+)
+
+// DomainPermissionSubscriptionGet returns one
+// domain permission subscription with the given id.
+func (p *Processor) DomainPermissionSubscriptionGet(
+ ctx context.Context,
+ id string,
+) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ permSubscription, 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 permSubscription == nil {
+ err := fmt.Errorf("domain permission subscription %s not found", id)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ return p.apiDomainPermSub(ctx, permSubscription)
+}
+
+// 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
+}
+
+func (p *Processor) DomainPermissionSubscriptionCreate(
+ ctx context.Context,
+ acct *gtsmodel.Account,
+ title string,
+ uri string,
+ contentType gtsmodel.DomainPermSubContentType,
+ permType gtsmodel.DomainPermissionType,
+ asDraft bool,
+ fetchUsername string,
+ fetchPassword string,
+) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
+ permSub := >smodel.DomainPermissionSubscription{
+ ID: id.NewULID(),
+ 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)
+}
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 750d4eec4..b08c112c3 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -2004,6 +2004,47 @@ func (c *Converter) DomainPermToAPIDomainPerm(
return domainPerm, nil
}
+func (c *Converter) DomainPermSubToAPIDomainPermSub(
+ ctx context.Context,
+ d *gtsmodel.DomainPermissionSubscription,
+) (*apimodel.DomainPermissionSubscription, error) {
+ // 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)
+ }
+
+ return &apimodel.DomainPermissionSubscription{
+ ID: d.ID,
+ Title: d.Title,
+ PermissionType: d.PermissionType.String(),
+ AsDraft: *d.AsDraft,
+ CreatedBy: d.CreatedByAccountID,
+ CreatedAt: util.FormatISO8601(d.CreatedAt),
+ URI: uri,
+ ContentType: d.ContentType.String(),
+ FetchUsername: d.FetchUsername,
+ FetchPassword: d.FetchPassword,
+ FetchedAt: fetchedAt,
+ SuccessfullyFetchedAt: successfullyFetchedAt,
+ Error: d.Error,
+ }, 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{
diff --git a/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts b/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts
new file mode 100644
index 000000000..0106118eb
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts
@@ -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 .
+*/
+
+import { gtsApi } from "../../gts-api";
+
+import type {
+ DomainPermSub,
+ DomainPermSubCreateParams,
+ DomainPermSubSearchParams,
+ DomainPermSubSearchResp,
+} from "../../../types/domain-permission";
+import parse from "parse-link-header";
+
+const extended = gtsApi.injectEndpoints({
+ endpoints: (build) => ({
+ searchDomainPermissionSubscriptions: build.query({
+ query: (form) => {
+ const params = new(URLSearchParams);
+ Object.entries(form).forEach(([k, v]) => {
+ if (v !== undefined) {
+ params.append(k, v);
+ }
+ });
+
+ let query = "";
+ if (params.size !== 0) {
+ query = `?${params.toString()}`;
+ }
+
+ return {
+ url: `/api/v1/admin/domain_permission_subscriptions${query}`
+ };
+ },
+ // Headers required for paging.
+ transformResponse: (apiResp: DomainPermSub[], meta) => {
+ const subs = apiResp;
+ const linksStr = meta?.response?.headers.get("Link");
+ const links = parse(linksStr);
+ return { subs, links };
+ },
+ // Only provide TRANSFORMED tag id since this model is not the same
+ // as getDomainPermissionSubscription model (due to transformResponse).
+ providesTags: [{ type: "DomainPermissionSubscription", id: "TRANSFORMED" }]
+ }),
+
+ getDomainPermissionSubscription: build.query({
+ query: (id) => ({
+ url: `/api/v1/admin/domain_permission_subscriptions/${id}`
+ }),
+ providesTags: (_result, _error, id) => [
+ { type: 'DomainPermissionSubscription', id }
+ ],
+ }),
+
+ createDomainPermissionSubscription: build.mutation({
+ query: (formData) => ({
+ method: "POST",
+ url: `/api/v1/admin/domain_permission_subscriptions`,
+ asForm: true,
+ body: formData,
+ discardEmpty: true
+ }),
+ invalidatesTags: [{ type: "DomainPermissionSubscription", id: "TRANSFORMED" }],
+ }),
+
+ }),
+});
+
+/**
+ * View domain permission subscriptions.
+ */
+const useLazySearchDomainPermissionSubscriptionsQuery = extended.useLazySearchDomainPermissionSubscriptionsQuery;
+
+/**
+ * Get domain permission subscription with the given ID.
+ */
+const useGetDomainPermissionSubscriptionQuery = extended.useGetDomainPermissionSubscriptionQuery;
+
+/**
+ * Create a domain permission subscription with the given parameters.
+ */
+const useCreateDomainPermissionSubscriptionMutation = extended.useCreateDomainPermissionSubscriptionMutation;
+
+export {
+ useLazySearchDomainPermissionSubscriptionsQuery,
+ useGetDomainPermissionSubscriptionQuery,
+ useCreateDomainPermissionSubscriptionMutation,
+};
diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts
index 9543819a9..34b66913a 100644
--- a/web/source/settings/lib/query/gts-api.ts
+++ b/web/source/settings/lib/query/gts-api.ts
@@ -170,7 +170,8 @@ export const gtsApi = createApi({
"DefaultInteractionPolicies",
"InteractionRequest",
"DomainPermissionDraft",
- "DomainPermissionExclude"
+ "DomainPermissionExclude",
+ "DomainPermissionSubscription"
],
endpoints: (build) => ({
instanceV1: build.query({
diff --git a/web/source/settings/lib/types/domain-permission.ts b/web/source/settings/lib/types/domain-permission.ts
index 1a0a9bd0b..93bfc9ca3 100644
--- a/web/source/settings/lib/types/domain-permission.ts
+++ b/web/source/settings/lib/types/domain-permission.ts
@@ -20,6 +20,7 @@
import typia from "typia";
import { PermType } from "./perm";
import { Links } from "parse-link-header";
+import { PermSubContentType } from "./permsubcontenttype";
export const validateDomainPerms = typia.createValidate();
@@ -213,3 +214,131 @@ export interface DomainPermExcludeCreateParams {
*/
private_comment?: string;
}
+
+/**
+ * API model of one domain permission susbcription.
+ */
+export interface DomainPermSub {
+ /**
+ * The ID of the domain permission subscription.
+ */
+ id: string;
+ /**
+ * Time at which the subscription was created (ISO 8601 Datetime).
+ */
+ created_at: string;
+ /**
+ * Title of this subscription, as set by admin who created or updated it.
+ */
+ title: string;
+ /**
+ * The type of domain permission subscription (allow, block).
+ */
+ permission_type: PermType;
+ /**
+ * 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.
+ */
+ as_draft: boolean;
+ /**
+ * ID of the account that created this subscription.
+ */
+ created_by: string;
+ /**
+ * URI to call in order to fetch the permissions list.
+ */
+ uri: string;
+ /**
+ * MIME content type to use when parsing the permissions list.
+ */
+ content_type: PermSubContentType;
+ /**
+ * (Optional) username to set for basic auth when doing a fetch of URI.
+ */
+ fetch_username?: string;
+ /**
+ * (Optional) password to set for basic auth when doing a fetch of URI.
+ */
+ fetch_password?: string;
+ /**
+ * Time at which the most recent fetch was attempted (ISO 8601 Datetime).
+ */
+ fetched_at?: string;
+ /**
+ * If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
+ */
+ error?: string;
+ /**
+ * Count of domain permission entries discovered at URI on last (successful) fetch.
+ */
+ count: number;
+}
+
+/**
+ * Parameters for GET to /api/v1/admin/domain_permission_subscriptions.
+ */
+export interface DomainPermSubSearchParams {
+ /**
+ * Return only block or allow subscriptions.
+ */
+ permission_type?: PermType;
+ /**
+ * Return only items *OLDER* than the given max ID (for paging downwards).
+ * The item with the specified ID will not be included in the response.
+ */
+ max_id?: string;
+ /**
+ * Return only items *NEWER* than the given since ID.
+ * The item with the specified ID will not be included in the response.
+ */
+ since_id?: string;
+ /**
+ * Return only items immediately *NEWER* than the given min ID (for paging upwards).
+ * The item with the specified ID will not be included in the response.
+ */
+ min_id?: string;
+ /**
+ * Number of items to return.
+ */
+ limit?: number;
+}
+
+/**
+ * Parameters for POST to /api/v1/admin/domain_permission_subscriptions.
+ */
+export interface DomainPermSubCreateParams {
+ /**
+ * Title of this subscription, as set by admin who created or updated it.
+ */
+ title?: string;
+ /**
+ * The type of domain permission subscription (allow, block).
+ */
+ permission_type: PermType;
+ /**
+ * URI to call in order to fetch the permissions list.
+ */
+ uri: string;
+ /**
+ * MIME content type to use when parsing the permissions list.
+ */
+ content_type: PermSubContentType;
+ /**
+ * 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.
+ */
+ as_draft?: boolean;
+ /**
+ * (Optional) username to set for basic auth when doing a fetch of URI.
+ */
+ fetch_username?: string;
+ /**
+ * (Optional) password to set for basic auth when doing a fetch of URI.
+ */
+ fetch_password?: string;
+}
+
+export interface DomainPermSubSearchResp {
+ subs: DomainPermSub[];
+ links: Links | null;
+}
diff --git a/web/source/settings/lib/types/permsubcontenttype.ts b/web/source/settings/lib/types/permsubcontenttype.ts
new file mode 100644
index 000000000..0468aae4d
--- /dev/null
+++ b/web/source/settings/lib/types/permsubcontenttype.ts
@@ -0,0 +1,20 @@
+/*
+ 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 .
+*/
+
+export type PermSubContentType = "text/plain" | "text/csv" | "application/json";
diff --git a/web/source/settings/lib/util/formvalidators.ts b/web/source/settings/lib/util/formvalidators.ts
index c509cf59d..358db616c 100644
--- a/web/source/settings/lib/util/formvalidators.ts
+++ b/web/source/settings/lib/util/formvalidators.ts
@@ -46,3 +46,22 @@ export function formDomainValidator(domain: string): string {
return "invalid domain";
}
+
+export function urlValidator(urlStr: string): string {
+ if (urlStr.length === 0) {
+ return "";
+ }
+
+ let url: URL;
+ try {
+ url = new URL(urlStr);
+ } catch (e) {
+ return e.message;
+ }
+
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
+ return `invalid protocol, must be http or https`;
+ }
+
+ return formDomainValidator(url.host);
+}
diff --git a/web/source/settings/style.css b/web/source/settings/style.css
index 740c30059..bb25c28dc 100644
--- a/web/source/settings/style.css
+++ b/web/source/settings/style.css
@@ -1360,9 +1360,11 @@ button.tab-button {
}
.domain-permission-drafts-view,
-.domain-permission-excludes-view {
+.domain-permission-excludes-view,
+.domain-permission-subscriptions-view {
.domain-permission-draft,
- .domain-permission-exclude {
+ .domain-permission-exclude,
+ .domain-permission-subscription {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
@@ -1404,14 +1406,17 @@ button.tab-button {
}
.domain-permission-draft-details,
-.domain-permission-exclude-details {
+.domain-permission-exclude-details,
+.domain-permission-subscription-details {
.info-list {
margin-top: 1rem;
}
}
.domain-permission-drafts-view,
-.domain-permission-draft-details {
+.domain-permission-draft-details,
+.domain-permission-subscriptions-view,
+.domain-permission-subscription-details {
dd.permission-type {
display: flex;
gap: 0.35rem;
diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx
new file mode 100644
index 000000000..b36ecbf85
--- /dev/null
+++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx
@@ -0,0 +1,40 @@
+/*
+ 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 .
+*/
+
+import React from "react";
+
+export function DomainPermissionSubscriptionHelpText() {
+ return (
+ <>
+ >
+ );
+}
+
+export function DomainPermissionSubscriptionDocsLink() {
+ return (
+
+ Learn more about domain permission subscriptions (opens in a new tab)
+
+ );
+}
diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/detail.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/detail.tsx
new file mode 100644
index 000000000..74acd685b
--- /dev/null
+++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/detail.tsx
@@ -0,0 +1,82 @@
+/*
+ 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 .
+*/
+
+import React from "react";
+import { useParams } from "wouter";
+import Loading from "../../../../components/loading";
+import { useBaseUrl } from "../../../../lib/navigation/util";
+import BackButton from "../../../../components/back-button";
+import { Error as ErrorC } from "../../../../components/error";
+import UsernameLozenge from "../../../../components/username-lozenge";
+import { useGetDomainPermissionSubscriptionQuery } from "../../../../lib/query/admin/domain-permissions/subscriptions";
+
+export default function DomainPermissionSubscriptionDetail() {
+ const baseUrl = useBaseUrl();
+ const backLocation: string = history.state?.backLocation ?? `~${baseUrl}`;
+
+ const params = useParams();
+ let id = params.permSubId as string | undefined;
+ if (!id) {
+ throw "no permSub ID";
+ }
+
+ const {
+ data: permSub,
+ isLoading,
+ isFetching,
+ isError,
+ error,
+ } = useGetDomainPermissionSubscriptionQuery(id);
+
+ if (isLoading || isFetching) {
+ return ;
+ } else if (isError) {
+ return ;
+ } else if (permSub === undefined) {
+ return ;
+ }
+
+ const created = permSub.created_at ? new Date(permSub.created_at).toDateString(): "unknown";
+
+ return (
+
+
Domain Permission Subscription Detail
+
+
+
Created
+
+
+
+
Created By
+
+
+
+
+
+
Pee pee
+
poo poo
+
+
+
+ );
+}
diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/index.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/index.tsx
new file mode 100644
index 000000000..da3102249
--- /dev/null
+++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/index.tsx
@@ -0,0 +1,234 @@
+/*
+ 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 .
+*/
+
+import React, { ReactNode, useEffect, useMemo } from "react";
+
+import { useTextInput } from "../../../../lib/form";
+import { PageableList } from "../../../../components/pageable-list";
+import MutationButton from "../../../../components/form/mutation-button";
+import { useLocation, useSearch } from "wouter";
+import { useLazySearchDomainPermissionSubscriptionsQuery } from "../../../../lib/query/admin/domain-permissions/subscriptions";
+import { DomainPermSub } from "../../../../lib/types/domain-permission";
+import { Error as ErrorC } from "../../../../components/error";
+import { Select, TextInput } from "../../../../components/form/inputs";
+import { formDomainValidator } from "../../../../lib/util/formvalidators";
+import { DomainPermissionSubscriptionDocsLink, DomainPermissionSubscriptionHelpText } from "./common";
+import { useCapitalize } from "../../../../lib/util";
+
+export default function DomainPermissionSubscriptionsSearch() {
+ return (
+
+
+
Domain Permission Subscriptions
+
+ You can use the form below to search through domain permission subscriptions.
+
+
+
+
+
+
+
+ );
+}
+
+function DomainPermissionSubscriptionsSearchForm() {
+ const [ location, setLocation ] = useLocation();
+ const search = useSearch();
+ const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]);
+ const hasParams = urlQueryParams.size != 0;
+ const [ searchSubscriptions, searchRes ] = useLazySearchDomainPermissionSubscriptionsQuery();
+
+ const form = {
+ domain: useTextInput("domain", {
+ defaultValue: urlQueryParams.get("domain") ?? "",
+ validator: formDomainValidator,
+ }),
+ limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "20" })
+ };
+
+ // On mount, if urlQueryParams were provided,
+ // trigger the search. For example, if page
+ // was accessed at /search?origin=local&limit=20,
+ // then run a search with origin=local and
+ // limit=20 and immediately render the results.
+ //
+ // If no urlQueryParams set, trigger default
+ // search (first page, no filtering).
+ useEffect(() => {
+ if (hasParams) {
+ searchSubscriptions(Object.fromEntries(urlQueryParams));
+ } else {
+ setLocation(location + "?limit=20");
+ }
+ }, [
+ urlQueryParams,
+ hasParams,
+ searchSubscriptions,
+ location,
+ setLocation,
+ ]);
+
+ // Rather than triggering the search directly,
+ // the "submit" button changes the location
+ // based on form field params, and lets the
+ // useEffect hook above actually do the search.
+ function submitQuery(e) {
+ e.preventDefault();
+
+ // Parse query parameters.
+ const entries = Object.entries(form).map(([k, v]) => {
+ // Take only defined form fields.
+ if (v.value === undefined || v.value.length === 0 || v.value === "any") {
+ return null;
+ }
+ return [[k, v.value]];
+ }).flatMap(kv => {
+ // Remove any nulls.
+ return kv || [];
+ });
+
+ const searchParams = new URLSearchParams(entries);
+ setLocation(location + "?" + searchParams.toString());
+ }
+
+ // Location to return to when user clicks "back" on the detail view.
+ const backLocation = location + (hasParams ? `?${urlQueryParams}` : "");
+
+ // Function to map an item to a list entry.
+ function itemToEntry(permSub: DomainPermSub): ReactNode {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+ No subscriptions found that match your query.}
+ prevNextLinks={searchRes.data?.links}
+ />
+ >
+ );
+}
+
+interface SubscriptionEntryProps {
+ permSub: DomainPermSub;
+ linkTo: string;
+ backLocation: string;
+}
+
+function SubscriptionListEntry({ permSub, linkTo, backLocation }: SubscriptionEntryProps) {
+ const [ _location, setLocation ] = useLocation();
+
+ const permType = permSub.permission_type;
+ if (!permType) {
+ return ;
+ }
+
+ const title = permSub.title !== "" ? permSub.title : "[none]";
+
+ return (
+ {
+ // When clicking on a subscription, direct
+ // to the detail view for that subscription.
+ setLocation(linkTo, {
+ // Store the back location in history so
+ // the detail view can use it to return to
+ // this page (including query parameters).
+ state: { backLocation: backLocation }
+ });
+ }}
+ role="link"
+ tabIndex={0}
+ >
+
+ { permSub.title !== "" &&
+
+ {title}
+
+ }
+
+
URL:
+
{permSub.uri}
+
+
+
URL:
+
{permSub.uri}
+
+
+
Permission type:
+
+
+ {permType}
+
+
+
+
+ );
+}
diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/new.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/new.tsx
new file mode 100644
index 000000000..df493ee20
--- /dev/null
+++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/new.tsx
@@ -0,0 +1,116 @@
+/*
+ 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 .
+*/
+
+import React from "react";
+import useFormSubmit from "../../../../lib/form/submit";
+import { useCreateDomainPermissionSubscriptionMutation } from "../../../../lib/query/admin/domain-permissions/subscriptions";
+import { useTextInput } from "../../../../lib/form";
+import { urlValidator } from "../../../../lib/util/formvalidators";
+import MutationButton from "../../../../components/form/mutation-button";
+import { Select, TextInput } from "../../../../components/form/inputs";
+import { useLocation } from "wouter";
+import { DomainPermissionSubscriptionDocsLink, DomainPermissionSubscriptionHelpText } from "./common";
+
+export default function DomainPermissionSubscriptionNew() {
+ const [ _location, setLocation ] = useLocation();
+
+ const form = {
+ uri: useTextInput("uri", {
+ validator: urlValidator,
+ }),
+ content_type: useTextInput("content_type", { defaultValue: "text/csv" }),
+ permission_type: useTextInput("permission_type", { defaultValue: "block" }),
+ title: useTextInput("title"),
+ };
+
+ const [formSubmit, result] = useFormSubmit(
+ form,
+ useCreateDomainPermissionSubscriptionMutation(),
+ {
+ changedOnly: false,
+ onFinish: (res) => {
+ if (res.data) {
+ // Creation successful,
+ // redirect to subscriptions overview.
+ setLocation(`/subscriptions/search`);
+ }
+ },
+ });
+
+ return (
+
+ );
+}
diff --git a/web/source/settings/views/moderation/menu.tsx b/web/source/settings/views/moderation/menu.tsx
index 7ac6f9327..b39a445d7 100644
--- a/web/source/settings/views/moderation/menu.tsx
+++ b/web/source/settings/views/moderation/menu.tsx
@@ -150,6 +150,23 @@ function ModerationDomainPermsMenu() {
icon="fa-plus"
/>
+
);
}
diff --git a/web/source/settings/views/moderation/router.tsx b/web/source/settings/views/moderation/router.tsx
index 779498ffe..d127aafb0 100644
--- a/web/source/settings/views/moderation/router.tsx
+++ b/web/source/settings/views/moderation/router.tsx
@@ -35,6 +35,9 @@ import DomainPermissionDraftDetail from "./domain-permissions/drafts/detail";
import DomainPermissionExcludeDetail from "./domain-permissions/excludes/detail";
import DomainPermissionExcludesSearch from "./domain-permissions/excludes";
import DomainPermissionExcludeNew from "./domain-permissions/excludes/new";
+import DomainPermissionSubscriptionsSearch from "./domain-permissions/subscriptions";
+import DomainPermissionSubscriptionNew from "./domain-permissions/subscriptions/new";
+import DomainPermissionSubscriptionDetail from "./domain-permissions/subscriptions/detail";
/*
EXPORTED COMPONENTS
@@ -151,6 +154,9 @@ function ModerationDomainPermsRouter() {
+
+
+