[feature] Create/update/remove domain permission subscriptions (#3623)

* [feature] Create/update/remove domain permission subscriptions

* lint

* envparsing

* remove errant fmt.Println

* create drafts, subs, exclude, from snapshot models

* name etag column correctly

* remove count column

* lint
This commit is contained in:
tobi 2025-01-05 13:20:33 +01:00 committed by GitHub
commit e9bb7ddd3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 4630 additions and 172 deletions

View file

@ -28,43 +28,47 @@ import (
)
const (
BasePath = "/v1/admin"
EmojiPath = BasePath + "/custom_emojis"
EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
EmojiCategoriesPath = EmojiPath + "/categories"
DomainBlocksPath = BasePath + "/domain_blocks"
DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
DomainAllowsPath = BasePath + "/domain_allows"
DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
DomainPermissionDraftsPath = BasePath + "/domain_permission_drafts"
DomainPermissionDraftsPathWithID = DomainPermissionDraftsPath + "/:" + apiutil.IDKey
DomainPermissionDraftAcceptPath = DomainPermissionDraftsPathWithID + "/accept"
DomainPermissionDraftRemovePath = DomainPermissionDraftsPathWithID + "/remove"
DomainPermissionExcludesPath = BasePath + "/domain_permission_excludes"
DomainPermissionExcludesPathWithID = DomainPermissionExcludesPath + "/:" + apiutil.IDKey
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
HeaderAllowsPath = BasePath + "/header_allows"
HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
HeaderBlocksPath = BasePath + "/header_blocks"
HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
AccountsV1Path = BasePath + "/accounts"
AccountsV2Path = "/v2/admin/accounts"
AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
AccountsActionPath = AccountsPathWithID + "/action"
AccountsApprovePath = AccountsPathWithID + "/approve"
AccountsRejectPath = AccountsPathWithID + "/reject"
MediaCleanupPath = BasePath + "/media_cleanup"
MediaRefetchPath = BasePath + "/media_refetch"
ReportsPath = BasePath + "/reports"
ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
ReportsResolvePath = ReportsPathWithID + "/resolve"
EmailPath = BasePath + "/email"
EmailTestPath = EmailPath + "/test"
InstanceRulesPath = BasePath + "/instance/rules"
InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
DebugPath = BasePath + "/debug"
DebugAPUrlPath = DebugPath + "/apurl"
DebugClearCachesPath = DebugPath + "/caches/clear"
BasePath = "/v1/admin"
EmojiPath = BasePath + "/custom_emojis"
EmojiPathWithID = EmojiPath + "/:" + apiutil.IDKey
EmojiCategoriesPath = EmojiPath + "/categories"
DomainBlocksPath = BasePath + "/domain_blocks"
DomainBlocksPathWithID = DomainBlocksPath + "/:" + apiutil.IDKey
DomainAllowsPath = BasePath + "/domain_allows"
DomainAllowsPathWithID = DomainAllowsPath + "/:" + apiutil.IDKey
DomainPermissionDraftsPath = BasePath + "/domain_permission_drafts"
DomainPermissionDraftsPathWithID = DomainPermissionDraftsPath + "/:" + apiutil.IDKey
DomainPermissionDraftAcceptPath = DomainPermissionDraftsPathWithID + "/accept"
DomainPermissionDraftRemovePath = DomainPermissionDraftsPathWithID + "/remove"
DomainPermissionExcludesPath = BasePath + "/domain_permission_excludes"
DomainPermissionExcludesPathWithID = DomainPermissionExcludesPath + "/:" + apiutil.IDKey
DomainPermissionSubscriptionsPath = BasePath + "/domain_permission_subscriptions"
DomainPermissionSubscriptionsPathWithID = DomainPermissionSubscriptionsPath + "/:" + apiutil.IDKey
DomainPermissionSubscriptionsPreviewPath = DomainPermissionSubscriptionsPath + "/preview"
DomainPermissionSubscriptionRemovePath = DomainPermissionSubscriptionsPathWithID + "/remove"
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
HeaderAllowsPath = BasePath + "/header_allows"
HeaderAllowsPathWithID = HeaderAllowsPath + "/:" + apiutil.IDKey
HeaderBlocksPath = BasePath + "/header_blocks"
HeaderBlocksPathWithID = HeaderBlocksPath + "/:" + apiutil.IDKey
AccountsV1Path = BasePath + "/accounts"
AccountsV2Path = "/v2/admin/accounts"
AccountsPathWithID = AccountsV1Path + "/:" + apiutil.IDKey
AccountsActionPath = AccountsPathWithID + "/action"
AccountsApprovePath = AccountsPathWithID + "/approve"
AccountsRejectPath = AccountsPathWithID + "/reject"
MediaCleanupPath = BasePath + "/media_cleanup"
MediaRefetchPath = BasePath + "/media_refetch"
ReportsPath = BasePath + "/reports"
ReportsPathWithID = ReportsPath + "/:" + apiutil.IDKey
ReportsResolvePath = ReportsPathWithID + "/resolve"
EmailPath = BasePath + "/email"
EmailTestPath = EmailPath + "/test"
InstanceRulesPath = BasePath + "/instance/rules"
InstanceRulesPathWithID = InstanceRulesPath + "/:" + apiutil.IDKey
DebugPath = BasePath + "/debug"
DebugAPUrlPath = DebugPath + "/apurl"
DebugClearCachesPath = DebugPath + "/caches/clear"
FilterQueryKey = "filter"
MaxShortcodeDomainKey = "max_shortcode_domain"
@ -118,6 +122,14 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
attachHandler(http.MethodGet, DomainPermissionExcludesPathWithID, m.DomainPermissionExcludeGETHandler)
attachHandler(http.MethodDelete, DomainPermissionExcludesPathWithID, m.DomainPermissionExcludeDELETEHandler)
// domain permission subscriptions stuff
attachHandler(http.MethodPost, DomainPermissionSubscriptionsPath, m.DomainPermissionSubscriptionPOSTHandler)
attachHandler(http.MethodGet, DomainPermissionSubscriptionsPath, m.DomainPermissionSubscriptionsGETHandler)
attachHandler(http.MethodGet, DomainPermissionSubscriptionsPreviewPath, m.DomainPermissionSubscriptionsPreviewGETHandler)
attachHandler(http.MethodGet, DomainPermissionSubscriptionsPathWithID, m.DomainPermissionSubscriptionGETHandler)
attachHandler(http.MethodPatch, DomainPermissionSubscriptionsPathWithID, m.DomainPermissionSubscriptionPATCHHandler)
attachHandler(http.MethodPost, DomainPermissionSubscriptionRemovePath, m.DomainPermissionSubscriptionRemovePOSTHandler)
// header filtering administration routes
attachHandler(http.MethodGet, HeaderAllowsPathWithID, m.HeaderFilterAllowGET)
attachHandler(http.MethodGet, HeaderBlocksPathWithID, m.HeaderFilterBlockGET)

View file

@ -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
}

View file

@ -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
}

View file

@ -149,7 +149,7 @@ func (m *Module) DomainPermissionDraftsGETHandler(c *gin.Context) {
permTypeStr := c.Query(apiutil.DomainPermissionPermTypeKey)
permType := gtsmodel.ParseDomainPermissionType(permTypeStr)
if permType == gtsmodel.DomainPermissionUnknown {
if permTypeStr != "" && permType == gtsmodel.DomainPermissionUnknown {
text := fmt.Sprintf(
"permission_type %s not recognized, valid values are empty string, block, or allow",
permTypeStr,

View file

@ -0,0 +1,244 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"net/url"
"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// DomainPermissionSubscriptionPOSTHandler swagger:operation POST /api/v1/admin/domain_permission_subscriptions domainPermissionSubscriptionCreate
//
// Create a domain permission subscription with the given parameters.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
// - application/json
//
// produces:
// - application/json
//
// parameters:
// -
// name: priority
// in: formData
// description: >-
// Priority of this subscription compared to others of the same permission type.
// 0-255 (higher = higher priority). Higher priority subscriptions will overwrite
// permissions generated by lower priority subscriptions. When two subscriptions
// have the same `priority` value, priority is indeterminate, so it's recommended
// to always set this value manually.
// type: number
// minimum: 0
// maximum: 255
// default: 0
// -
// name: title
// in: formData
// description: Optional title for this subscription.
// type: string
// -
// name: permission_type
// required: true
// in: formData
// description: >-
// Type of permissions to create by parsing the targeted file/list.
// One of "allow" or "block".
// type: string
// -
// name: as_draft
// in: formData
// description: >-
// If true, domain permissions arising from this subscription will be
// created as drafts that must be approved by a moderator to take effect.
// If false, domain permissions from this subscription will come into force immediately.
// Defaults to "true".
// type: boolean
// default: true
// -
// name: adopt_orphans
// in: formData
// description: >-
// If true, this domain permission subscription will "adopt" domain permissions
// which already exist on the instance, and which meet the following conditions:
// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
// in the subscribed list. Such orphaned domain permissions will be given this
// subscription's subscription ID value and be managed by this subscription.
// type: boolean
// default: false
// -
// name: uri
// required: true
// in: formData
// description: URI to call in order to fetch the permissions list.
// type: string
// -
// name: content_type
// required: true
// in: formData
// description: >-
// MIME content type to use when parsing the permissions list.
// One of "text/plain", "text/csv", and "application/json".
// type: string
// -
// name: fetch_username
// in: formData
// description: >-
// Optional basic auth username to provide when fetching given uri.
// If set, will be transmitted along with `fetch_password` when doing the fetch.
// type: string
// -
// name: fetch_password
// in: formData
// description: >-
// Optional basic auth password to provide when fetching given uri.
// If set, will be transmitted along with `fetch_username` when doing the fetch.
// type: string
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The newly created domain permission subscription.
// schema:
// "$ref": "#/definitions/domainPermissionSubscription"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '406':
// description: not acceptable
// '409':
// description: conflict
// '500':
// description: internal server error
func (m *Module) DomainPermissionSubscriptionPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
// Parse + validate form.
form := new(apimodel.DomainPermissionSubscriptionRequest)
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
// Check priority.
// Default to 0.
priority := util.PtrOrZero(form.Priority)
if priority < 0 || priority > 255 {
const errText = "priority must be a number in the range 0 to 255"
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Ensure URI is set.
if form.URI == nil {
const errText = "uri must be set"
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Ensure URI is parseable.
uri, err := url.Parse(*form.URI)
if err != nil {
err := fmt.Errorf("invalid uri provided: %w", err)
errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Normalize URI by converting back to string.
uriStr := uri.String()
// Content type must be set.
contentTypeStr := util.PtrOrZero(form.ContentType)
contentType, errWithCode := parseDomainPermSubContentType(contentTypeStr)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Permission type must be set.
permTypeStr := util.PtrOrZero(form.PermissionType)
permType, errWithCode := parseDomainPermissionType(permTypeStr)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Default `as_draft` to true.
asDraft := util.PtrOrValue(form.AsDraft, true)
permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionCreate(
c.Request.Context(),
authed.Account,
uint8(priority), // #nosec G115 -- Validated above.
util.PtrOrZero(form.Title), // Optional.
uriStr,
contentType,
permType,
asDraft,
util.PtrOrZero(form.FetchUsername), // Optional.
util.PtrOrZero(form.FetchPassword), // Optional.
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, permSub)
}

View file

@ -0,0 +1,104 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package admin
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// 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)
}

View file

@ -0,0 +1,143 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package admin
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// DomainPermissionSubscriptionRemovePOSTHandler swagger:operation POST /api/v1/admin/domain_permission_subscriptions/{id}/remove domainPermissionSubscriptionRemove
//
// Remove a domain permission subscription.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
// - application/json
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// required: true
// in: path
// description: ID of the domain permission subscription.
// type: string
// -
// name: remove_children
// in: formData
// description: >-
// When removing the domain permission subscription, also
// remove children of this subscription, ie., domain permissions
// that are managed by this subscription. If false, then children
// will instead be orphaned but not removed.
//
// Note that removed permissions may end up being created again later
// by another domain permission subscription of lower priority than
// the removed subscription. Likewise, orphaned children may be later
// adopted by another subscription.
// type: boolean
// default: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The removed domain permission subscription.
// schema:
// "$ref": "#/definitions/domainPermissionSubscription"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '406':
// description: not acceptable
// '409':
// description: conflict
// '500':
// description: internal server error
func (m *Module) DomainPermissionSubscriptionRemovePOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
type RemoveForm struct {
RemoveChildren *bool `json:"remove_children" form:"remove_children"`
}
form := new(RemoveForm)
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
// Default removeChildren to true.
removeChildren := util.PtrOrValue(form.RemoveChildren, true)
permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionRemove(
c.Request.Context(),
id,
removeChildren,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, permSub)
}

View file

@ -0,0 +1,177 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/paging"
)
// DomainPermissionSubscriptionsGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions domainPermissionSubscriptionsGet
//
// View domain permission subscriptions.
//
// The subscriptions will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
//
// The next and previous queries can be parsed from the returned Link header.
//
// Example:
//
// ```
// <https://example.org/api/v1/admin/domain_permission_subscriptions?limit=20&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/domain_permission_subscriptions?limit=20&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
// ````
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
// -
// name: permission_type
// type: string
// description: Filter on "block" or "allow" type subscriptions.
// in: query
// -
// name: max_id
// type: string
// description: >-
// Return only items *OLDER* than the given max ID (for paging downwards).
// The item with the specified ID will not be included in the response.
// in: query
// -
// name: since_id
// type: string
// description: >-
// Return only items *NEWER* than the given since ID.
// The item with the specified ID will not be included in the response.
// in: query
// -
// name: min_id
// type: string
// description: >-
// Return only items immediately *NEWER* than the given min ID (for paging upwards).
// The item with the specified ID will not be included in the response.
// in: query
// -
// name: limit
// type: integer
// description: Number of items to return.
// default: 20
// minimum: 1
// maximum: 100
// in: query
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: Domain permission subscriptions.
// schema:
// type: array
// items:
// "$ref": "#/definitions/domainPermissionSubscription"
// headers:
// Link:
// type: string
// description: Links to the next and previous queries.
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) DomainPermissionSubscriptionsGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
permType := c.Query(apiutil.DomainPermissionPermTypeKey)
switch permType {
case "", "block", "allow":
// No problem.
default:
// Invalid.
text := fmt.Sprintf(
"permission_type %s not recognized, valid values are empty string, block, or allow",
permType,
)
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
page, errWithCode := paging.ParseIDPage(c, 1, 200, 20)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
resp, errWithCode := m.processor.Admin().DomainPermissionSubscriptionsGet(
c.Request.Context(),
gtsmodel.ParseDomainPermissionType(permType),
page,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
apiutil.JSON(c, http.StatusOK, resp.Items)
}

View file

@ -0,0 +1,132 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// DomainPermissionSubscriptionsPreviewGETHandler swagger:operation GET /api/v1/admin/domain_permission_subscriptions/preview domainPermissionSubscriptionsPreviewGet
//
// View all domain permission subscriptions of the given permission type, in priority order (highest to lowest).
//
// This view allows you to see the order in which domain permissions will actually be fetched and created.
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
// -
// name: permission_type
// type: string
// description: Filter on "block" or "allow" type subscriptions.
// in: query
// required: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: Domain permission subscriptions.
// schema:
// type: array
// items:
// "$ref": "#/definitions/domainPermissionSubscription"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) DomainPermissionSubscriptionsPreviewGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
permType := c.Query(apiutil.DomainPermissionPermTypeKey)
switch permType {
case "block", "allow":
// No problem.
case "":
// Not set.
const text = "permission_type must be set, valid values are block or allow"
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
default:
// Invalid.
text := fmt.Sprintf(
"permission_type %s not recognized, valid values are block or allow",
permType,
)
errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
resp, errWithCode := m.processor.Admin().DomainPermissionSubscriptionsGetByPriority(
c.Request.Context(),
gtsmodel.ParseDomainPermissionType(permType),
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, resp)
}

View file

@ -0,0 +1,254 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package admin
import (
"errors"
"fmt"
"net/http"
"net/url"
"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// DomainPermissionSubscriptionPATCHHandler swagger:operation PATCH /api/v1/admin/domain_permission_subscriptions/${id} domainPermissionSubscriptionUpdate
//
// Update a domain permission subscription with the given parameters.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
// - application/json
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// required: true
// in: path
// description: ID of the domain permission subscription.
// type: string
// -
// name: priority
// in: formData
// description: >-
// Priority of this subscription compared to others of the same permission type.
// 0-255 (higher = higher priority). Higher priority subscriptions will overwrite
// permissions generated by lower priority subscriptions. When two subscriptions
// have the same `priority` value, priority is indeterminate, so it's recommended
// to always set this value manually.
// type: number
// minimum: 0
// maximum: 255
// -
// name: title
// in: formData
// description: Optional title for this subscription.
// type: string
// -
// name: uri
// in: formData
// description: URI to call in order to fetch the permissions list.
// type: string
// -
// name: as_draft
// in: formData
// description: >-
// If true, domain permissions arising from this subscription will be
// created as drafts that must be approved by a moderator to take effect.
// If false, domain permissions from this subscription will come into force immediately.
// Defaults to "true".
// type: boolean
// default: true
// -
// name: adopt_orphans
// in: formData
// description: >-
// If true, this domain permission subscription will "adopt" domain permissions
// which already exist on the instance, and which meet the following conditions:
// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
// in the subscribed list. Such orphaned domain permissions will be given this
// subscription's subscription ID value and be managed by this subscription.
// type: boolean
// default: false
// -
// name: content_type
// in: formData
// description: >-
// MIME content type to use when parsing the permissions list.
// One of "text/plain", "text/csv", and "application/json".
// type: string
// -
// name: fetch_username
// in: formData
// description: >-
// Optional basic auth username to provide when fetching given uri.
// If set, will be transmitted along with `fetch_password` when doing the fetch.
// type: string
// -
// name: fetch_password
// in: formData
// description: >-
// Optional basic auth password to provide when fetching given uri.
// If set, will be transmitted along with `fetch_username` when doing the fetch.
// type: string
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The updated domain permission subscription.
// schema:
// "$ref": "#/definitions/domainPermissionSubscription"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '406':
// description: not acceptable
// '409':
// description: conflict
// '500':
// description: internal server error
func (m *Module) DomainPermissionSubscriptionPATCHHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
}
if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
return
}
if authed.Account.IsMoving() {
apiutil.ForbiddenAfterMove(c)
return
}
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Parse + validate form.
form := new(apimodel.DomainPermissionSubscriptionRequest)
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
// Normalize priority if set.
var priority *uint8
if form.Priority != nil {
prioInt := *form.Priority
if prioInt < 0 || prioInt > 255 {
const errText = "priority must be a number in the range 0 to 255"
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
priority = util.Ptr(uint8(prioInt)) // #nosec G115 -- Just validated.
}
// Validate URI if set.
var uriStr *string
if form.URI != nil {
uri, err := url.Parse(*form.URI)
if err != nil {
err := fmt.Errorf("invalid uri provided: %w", err)
errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
// Normalize URI by converting back to string.
uriStr = util.Ptr(uri.String())
}
// Validate content type if set.
var contentType *gtsmodel.DomainPermSubContentType
if form.ContentType != nil {
ct, errWithCode := parseDomainPermSubContentType(*form.ContentType)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
contentType = &ct
}
// Make sure at least one field is set,
// otherwise we're trying to update nothing.
if priority == nil &&
form.Title == nil &&
uriStr == nil &&
contentType == nil &&
form.AsDraft == nil &&
form.AdoptOrphans == nil &&
form.FetchUsername == nil &&
form.FetchPassword == nil {
const errText = "no updateable fields set on request"
errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText)
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
permSub, errWithCode := m.processor.Admin().DomainPermissionSubscriptionUpdate(
c.Request.Context(),
id,
priority,
form.Title,
uriStr,
contentType,
form.AsDraft,
form.AdoptOrphans,
form.FetchUsername,
form.FetchPassword,
)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSON(c, http.StatusOK, permSub)
}

View file

@ -99,3 +99,101 @@ type DomainKeysExpireRequest struct {
// hostname/domain to expire keys for.
Domain string `form:"domain" json:"domain"`
}
// DomainPermissionSubscription represents an auto-refreshing subscription to a list of domain permissions (allows, blocks).
//
// swagger:model domainPermissionSubscription
type DomainPermissionSubscription struct {
// The ID of the domain permission subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
ID string `json:"id"`
// Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
// example: 100
Priority uint8 `json:"priority"`
// Title of this subscription, as set by admin who created or updated it.
// example: really cool list of neato pals
Title string `json:"title"`
// The type of domain permission subscription (allow, block).
// example: block
PermissionType string `json:"permission_type"`
// If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. If false, domain permissions from this subscription will come into force immediately.
// example: true
AsDraft bool `json:"as_draft"`
// If true, this domain permission subscription will "adopt" domain permissions which already exist on the instance, and which meet the following conditions: 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present in the subscribed list. Such orphaned domain permissions will be given this subscription's subscription ID value.
// example: false
AdoptOrphans bool `json:"adopt_orphans"`
// Time at which the subscription was created (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
CreatedAt string `json:"created_at"`
// ID of the account that created this subscription.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
CreatedBy string `json:"created_by"`
// URI to call in order to fetch the permissions list.
// example: https://www.example.org/blocklists/list1.csv
URI string `json:"uri"`
// MIME content type to use when parsing the permissions list.
// example: text/csv
ContentType string `json:"content_type"`
// (Optional) username to set for basic auth when doing a fetch of URI.
// example: admin123
FetchUsername string `json:"fetch_username,omitempty"`
// (Optional) password to set for basic auth when doing a fetch of URI.
// example: admin123
FetchPassword string `json:"fetch_password,omitempty"`
// Time of the most recent fetch attempt (successful or otherwise) (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
// readonly: true
FetchedAt string `json:"fetched_at,omitempty"`
// Time of the most recent successful fetch (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
// readonly: true
SuccessfullyFetchedAt string `json:"successfully_fetched_at,omitempty"`
// If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt.
// example: Oopsie doopsie, we made a fucky wucky.
// readonly: true
Error string `json:"error,omitempty"`
// Count of domain permission entries discovered at URI on last (successful) fetch.
// example: 53
// readonly: true
Count uint64 `json:"count"`
}
// DomainPermissionSubscriptionRequest represents a request to create or update a domain permission subscription..
//
// swagger:ignore
type DomainPermissionSubscriptionRequest struct {
// Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
// example: 100
Priority *int `form:"priority" json:"priority"`
// Title of this subscription, as set by admin who created or updated it.
// example: really cool list of neato pals
Title *string `form:"title" json:"title"`
// The type of domain permission subscription (allow, block).
// example: block
PermissionType *string `form:"permission_type" json:"permission_type"`
// URI to call in order to fetch the permissions list.
// example: https://www.example.org/blocklists/list1.csv
URI *string `form:"uri" json:"uri"`
// MIME content type to use when parsing the permissions list.
// example: text/csv
ContentType *string `form:"content_type" json:"content_type"`
// If true, domain permissions arising from this subscription will be
// created as drafts that must be approved by a moderator to take effect.
// If false, domain permissions from this subscription will come into force immediately.
// example: true
AsDraft *bool `form:"as_draft" json:"as_draft"`
// If true, this domain permission subscription will "adopt" domain permissions
// which already exist on the instance, and which meet the following conditions:
// 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present
// in the subscribed list. Such orphaned domain permissions will be given this
// subscription's subscription ID value and be managed by this subscription.
AdoptOrphans *bool `form:"adopt_orphans" json:"adopt_orphans"`
// (Optional) username to set for basic auth when doing a fetch of URI.
// example: admin123
FetchUsername *string `form:"fetch_username" json:"fetch_username"`
// (Optional) password to set for basic auth when doing a fetch of URI.
// example: admin123
FetchPassword *string `form:"fetch_password" json:"fetch_password"`
}

View file

@ -75,6 +75,7 @@ func (c *Caches) Init() {
c.initDomainAllow()
c.initDomainBlock()
c.initDomainPermissionDraft()
c.initDomainPermissionSubscription()
c.initDomainPermissionExclude()
c.initEmoji()
c.initEmojiCategory()

34
internal/cache/db.go vendored
View file

@ -70,6 +70,9 @@ type DBCaches struct {
// DomainPermissionDraft provides access to the domain permission draft database cache.
DomainPermissionDraft StructCache[*gtsmodel.DomainPermissionDraft]
// DomainPermissionSubscription provides access to the domain permission subscription database cache.
DomainPermissionSubscription StructCache[*gtsmodel.DomainPermissionSubscription]
// DomainPermissionExclude provides access to the domain permission exclude database cache.
DomainPermissionExclude *domain.Cache
@ -589,6 +592,37 @@ func (c *Caches) initDomainPermissionDraft() {
})
}
func (c *Caches) initDomainPermissionSubscription() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
sizeofDomainPermissionSubscription(), // model in-mem size.
config.GetCacheDomainPermissionSubscriptionMemRation(),
)
log.Infof(nil, "cache size = %d", cap)
copyF := func(d1 *gtsmodel.DomainPermissionSubscription) *gtsmodel.DomainPermissionSubscription {
d2 := new(gtsmodel.DomainPermissionSubscription)
*d2 = *d1
// Don't include ptr fields that
// will be populated separately.
d2.CreatedByAccount = nil
return d2
}
c.DB.DomainPermissionSubscription.Init(structr.CacheConfig[*gtsmodel.DomainPermissionSubscription]{
Indices: []structr.IndexConfig{
{Fields: "ID"},
{Fields: "URI"},
},
MaxSize: cap,
IgnoreErr: ignoreErrors,
Copy: copyF,
})
}
func (c *Caches) initDomainPermissionExclude() {
c.DB.DomainPermissionExclude = new(domain.Cache)
}

View file

@ -357,6 +357,25 @@ func sizeofDomainPermissionDraft() uintptr {
}))
}
func sizeofDomainPermissionSubscription() uintptr {
return uintptr(size.Of(&gtsmodel.DomainPermissionSubscription{
ID: exampleID,
Priority: uint8(255),
Title: exampleTextSmall,
PermissionType: gtsmodel.DomainPermissionBlock,
AsDraft: util.Ptr(true),
CreatedByAccountID: exampleID,
URI: exampleURI,
ContentType: gtsmodel.DomainPermSubContentTypeCSV,
FetchUsername: "username",
FetchPassword: "password",
FetchedAt: exampleTime,
SuccessfullyFetchedAt: exampleTime,
ETag: exampleID,
Error: exampleTextSmall,
}))
}
func sizeofEmoji() uintptr {
return uintptr(size.Of(&gtsmodel.Emoji{
ID: exampleID,

View file

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

View file

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

View file

@ -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()

View file

@ -0,0 +1,354 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package bundb
import (
"context"
"errors"
"slices"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/uptrace/bun"
)
func (d *domainDB) getDomainPermissionSubscription(
ctx context.Context,
lookup string,
dbQuery func(*gtsmodel.DomainPermissionSubscription) error,
keyParts ...any,
) (*gtsmodel.DomainPermissionSubscription, error) {
// Fetch perm subscription from database cache with loader callback.
permSub, err := d.state.Caches.DB.DomainPermissionSubscription.LoadOne(
lookup,
// Only called if not cached.
func() (*gtsmodel.DomainPermissionSubscription, error) {
var permSub gtsmodel.DomainPermissionSubscription
if err := dbQuery(&permSub); err != nil {
return nil, err
}
return &permSub, nil
},
keyParts...,
)
if err != nil {
return nil, err
}
if gtscontext.Barebones(ctx) {
// No need to fully populate.
return permSub, nil
}
if permSub.CreatedByAccount == nil {
// Not set, fetch from database.
permSub.CreatedByAccount, err = d.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
permSub.CreatedByAccountID,
)
if err != nil {
return nil, gtserror.Newf("error populating created by account: %w", err)
}
}
return permSub, nil
}
func (d *domainDB) GetDomainPermissionSubscriptionByID(
ctx context.Context,
id string,
) (*gtsmodel.DomainPermissionSubscription, error) {
return d.getDomainPermissionSubscription(
ctx,
"ID",
func(permSub *gtsmodel.DomainPermissionSubscription) error {
return d.db.
NewSelect().
Model(permSub).
Where("? = ?", bun.Ident("domain_permission_subscription.id"), id).
Scan(ctx)
},
id,
)
}
func (d *domainDB) GetDomainPermissionSubscriptions(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
page *paging.Page,
) (
[]*gtsmodel.DomainPermissionSubscription,
error,
) {
var (
// Get paging params.
minID = page.GetMin()
maxID = page.GetMax()
limit = page.GetLimit()
order = page.GetOrder()
// Make educated guess for slice size
permSubIDs = make([]string, 0, limit)
)
q := d.db.
NewSelect().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_subscriptions"),
bun.Ident("domain_permission_subscription"),
).
// Select only IDs from table
Column("domain_permission_subscription.id")
// Return only items with id
// lower than provided maxID.
if maxID != "" {
q = q.Where(
"? < ?",
bun.Ident("domain_permission_subscription.id"),
maxID,
)
}
// Return only items with id
// greater than provided minID.
if minID != "" {
q = q.Where(
"? > ?",
bun.Ident("domain_permission_subscription.id"),
minID,
)
}
// Return only items with
// given permission type.
if permType != gtsmodel.DomainPermissionUnknown {
q = q.Where(
"? = ?",
bun.Ident("domain_permission_subscription.permission_type"),
permType,
)
}
if limit > 0 {
// Limit amount of
// items returned.
q = q.Limit(limit)
}
if order == paging.OrderAscending {
// Page up.
q = q.OrderExpr(
"? ASC",
bun.Ident("domain_permission_subscription.id"),
)
} else {
// Page down.
q = q.OrderExpr(
"? DESC",
bun.Ident("domain_permission_subscription.id"),
)
}
if err := q.Scan(ctx, &permSubIDs); err != nil {
return nil, err
}
// Catch case of no items early
if len(permSubIDs) == 0 {
return nil, db.ErrNoEntries
}
// If we're paging up, we still want items
// to be sorted by ID desc, so reverse slice.
if order == paging.OrderAscending {
slices.Reverse(permSubIDs)
}
// Allocate return slice (will be at most len permSubIDs).
permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs))
for _, id := range permSubIDs {
permSub, err := d.GetDomainPermissionSubscriptionByID(ctx, id)
if err != nil {
log.Errorf(ctx, "error getting domain permission subscription %q: %v", id, err)
continue
}
// Append to return slice
permSubs = append(permSubs, permSub)
}
return permSubs, nil
}
func (d *domainDB) GetDomainPermissionSubscriptionsByPriority(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
) (
[]*gtsmodel.DomainPermissionSubscription,
error,
) {
permSubIDs := []string{}
q := d.db.
NewSelect().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_subscriptions"),
bun.Ident("domain_permission_subscription"),
).
// Select only IDs from table
Column("domain_permission_subscription.id").
// Select only subs of given perm type.
Where(
"? = ?",
bun.Ident("domain_permission_subscription.permission_type"),
permType,
).
// Order by priority descending.
OrderExpr(
"? DESC",
bun.Ident("domain_permission_subscription.priority"),
)
if err := q.Scan(ctx, &permSubIDs); err != nil {
return nil, err
}
// Catch case of no items early
if len(permSubIDs) == 0 {
return nil, db.ErrNoEntries
}
// Allocate return slice (will be at most len permSubIDs).
permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs))
for _, id := range permSubIDs {
permSub, err := d.GetDomainPermissionSubscriptionByID(ctx, id)
if err != nil {
log.Errorf(ctx, "error getting domain permission subscription %q: %v", id, err)
continue
}
// Append to return slice
permSubs = append(permSubs, permSub)
}
return permSubs, nil
}
func (d *domainDB) PutDomainPermissionSubscription(
ctx context.Context,
permSubscription *gtsmodel.DomainPermissionSubscription,
) error {
return d.state.Caches.DB.DomainPermissionSubscription.Store(
permSubscription,
func() error {
_, err := d.db.
NewInsert().
Model(permSubscription).
Exec(ctx)
return err
},
)
}
func (d *domainDB) UpdateDomainPermissionSubscription(
ctx context.Context,
permSubscription *gtsmodel.DomainPermissionSubscription,
columns ...string,
) error {
return d.state.Caches.DB.DomainPermissionSubscription.Store(
permSubscription,
func() error {
_, err := d.db.
NewUpdate().
Model(permSubscription).
Where("? = ?", bun.Ident("id"), permSubscription.ID).
Column(columns...).
Exec(ctx)
return err
},
)
}
func (d *domainDB) DeleteDomainPermissionSubscription(
ctx context.Context,
id string,
) error {
// Delete the permSub from DB.
q := d.db.NewDelete().
TableExpr(
"? AS ?",
bun.Ident("domain_permission_subscriptions"),
bun.Ident("domain_permission_subscription"),
).
Where(
"? = ?",
bun.Ident("domain_permission_subscription.id"),
id,
)
_, err := q.Exec(ctx)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return err
}
// Invalidate any cached model by ID.
d.state.Caches.DB.DomainPermissionSubscription.Invalidate("ID", id)
return nil
}
func (d *domainDB) CountDomainPermissionSubscriptionPerms(
ctx context.Context,
id string,
) (int, error) {
permSubscription, err := d.GetDomainPermissionSubscriptionByID(
gtscontext.SetBarebones(ctx),
id,
)
if err != nil {
return 0, err
}
q := d.db.NewSelect()
if permSubscription.PermissionType == gtsmodel.DomainPermissionBlock {
q = q.TableExpr(
"? AS ?",
bun.Ident("domain_blocks"),
bun.Ident("perm"),
)
} else {
q = q.TableExpr(
"? AS ?",
bun.Ident("domain_allows"),
bun.Ident("perm"),
)
}
return q.
Column("perm.id").
Where("? = ?", bun.Ident("perm.subscription_id"), id).
Count(ctx)
}

View file

@ -0,0 +1,99 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package bundb_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type DomainPermissionSubscriptionTestSuite struct {
BunDBStandardTestSuite
}
func (suite *DomainPermissionSubscriptionTestSuite) TestCount() {
var (
ctx = context.Background()
testAccount = suite.testAccounts["admin_account"]
permSub = &gtsmodel.DomainPermissionSubscription{
ID: "01JGV3VZ72K58BYW8H5GEVBZGN",
PermissionType: gtsmodel.DomainPermissionBlock,
CreatedByAccountID: testAccount.ID,
CreatedByAccount: testAccount,
URI: "https://example.org/whatever.csv",
ContentType: gtsmodel.DomainPermSubContentTypeCSV,
}
perms = []*gtsmodel.DomainBlock{
{
ID: "01JGV42G72YCKN06AC51RZPFES",
Domain: "whatever.com",
CreatedByAccountID: testAccount.ID,
CreatedByAccount: testAccount,
SubscriptionID: permSub.ID,
},
{
ID: "01JGV43ZQKYPHM2M0YBQDFDSD1",
Domain: "aaaa.example.org",
CreatedByAccountID: testAccount.ID,
CreatedByAccount: testAccount,
SubscriptionID: permSub.ID,
},
{
ID: "01JGV444KDDC4WFG6MZQVM0N2Z",
Domain: "bbbb.example.org",
CreatedByAccountID: testAccount.ID,
CreatedByAccount: testAccount,
SubscriptionID: permSub.ID,
},
{
ID: "01JGV44AFEMBWS6P6S72BQK376",
Domain: "cccc.example.org",
CreatedByAccountID: testAccount.ID,
CreatedByAccount: testAccount,
SubscriptionID: permSub.ID,
},
}
)
// Whack the perm sub in the DB.
if err := suite.state.DB.PutDomainPermissionSubscription(ctx, permSub); err != nil {
suite.FailNow(err.Error())
}
// Whack the perms in the db.
for _, perm := range perms {
if err := suite.state.DB.CreateDomainBlock(ctx, perm); err != nil {
suite.FailNow(err.Error())
}
}
// Count 'em.
count, err := suite.state.DB.CountDomainPermissionSubscriptionPerms(ctx, permSub.ID)
if err != nil {
suite.FailNow(err.Error())
}
suite.Equal(4, count)
}
func TestDomainPermissionSubscriptionTestSuite(t *testing.T) {
suite.Run(t, new(DomainPermissionSubscriptionTestSuite))
}

View file

@ -20,7 +20,7 @@ package migrations
import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241022153016_domain_permission_draft_exclude"
"github.com/uptrace/bun"
)

View file

@ -0,0 +1,33 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package gtsmodel
import "time"
type DomainPermissionDraft struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
PermissionType uint8 `bun:",notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
Domain string `bun:",nullzero,notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
PrivateComment string `bun:",nullzero"`
PublicComment string `bun:",nullzero"`
Obfuscate *bool `bun:",nullzero,notnull,default:false"`
SubscriptionID string `bun:"type:CHAR(26),unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"`
}

View file

@ -0,0 +1,31 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package gtsmodel
import (
"time"
)
type DomainPermissionExclude struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`
Domain string `bun:",nullzero,notnull,unique"`
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
PrivateComment string `bun:",nullzero"`
}

View file

@ -0,0 +1,75 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Create `domain_permission_subscriptions`.
if _, err := tx.
NewCreateTable().
Model((*gtsmodel.DomainPermissionSubscription)(nil)).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Create indexes. Indices. Indie sexes.
if _, err := tx.
NewCreateIndex().
Table("domain_permission_subscriptions").
// Filter on permission type.
Index("domain_permission_subscriptions_permission_type_idx").
Column("permission_type").
IfNotExists().
Exec(ctx); err != nil {
return err
}
if _, err := tx.
NewCreateIndex().
Table("domain_permission_subscriptions").
// Sort by priority DESC.
Index("domain_permission_subscriptions_priority_order_idx").
ColumnExpr("? DESC", bun.Ident("priority")).
IfNotExists().
Exec(ctx); err != nil {
return err
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -0,0 +1,38 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package gtsmodel
import "time"
type DomainPermissionSubscription struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`
Priority uint8 `bun:""`
Title string `bun:",nullzero,unique"`
PermissionType uint8 `bun:",nullzero,notnull"`
AsDraft *bool `bun:",nullzero,notnull,default:true"`
AdoptOrphans *bool `bun:",nullzero,notnull,default:false"`
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"`
URI string `bun:",nullzero,notnull,unique"`
ContentType uint16 `bun:",nullzero,notnull"`
FetchUsername string `bun:",nullzero"`
FetchPassword string `bun:",nullzero"`
FetchedAt time.Time `bun:"type:timestamptz,nullzero"`
SuccessfullyFetchedAt time.Time `bun:"type:timestamptz,nullzero"`
ETag string `bun:"etag,nullzero"`
Error string `bun:",nullzero"`
}

View file

@ -132,4 +132,44 @@ type Domain interface {
// IsDomainPermissionExcluded returns true if the given domain matches in the list of excluded domains.
IsDomainPermissionExcluded(ctx context.Context, domain string) (bool, error)
/*
Domain permission subscription stuff.
*/
// GetDomainPermissionSubscriptionByID gets one DomainPermissionSubscription with the given ID.
GetDomainPermissionSubscriptionByID(ctx context.Context, id string) (*gtsmodel.DomainPermissionSubscription, error)
// GetDomainPermissionSubscriptions returns a page of
// DomainPermissionSubscriptions using the given parameters.
GetDomainPermissionSubscriptions(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
page *paging.Page,
) ([]*gtsmodel.DomainPermissionSubscription, error)
// GetDomainPermissionSubscriptionsByPriority returns *all* domain permission
// subscriptions of the given permission type, sorted by priority descending.
GetDomainPermissionSubscriptionsByPriority(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
) ([]*gtsmodel.DomainPermissionSubscription, error)
// PutDomainPermissionSubscription stores one DomainPermissionSubscription.
PutDomainPermissionSubscription(ctx context.Context, permSub *gtsmodel.DomainPermissionSubscription) error
// UpdateDomainPermissionSubscription updates the provided
// columns of one DomainPermissionSubscription.
UpdateDomainPermissionSubscription(
ctx context.Context,
permSub *gtsmodel.DomainPermissionSubscription,
columns ...string,
) error
// DeleteDomainPermissionSubscription deletes one DomainPermissionSubscription with the given id.
DeleteDomainPermissionSubscription(ctx context.Context, id string) error
// CountDomainPermissionSubscriptionPerms counts the number of permissions
// currently managed by the domain permission subscription of the given ID.
CountDomainPermissionSubscriptionPerms(ctx context.Context, id string) (int, error)
}

View file

@ -0,0 +1,74 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package gtsmodel
import "time"
type DomainPermissionSubscription struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
Priority uint8 `bun:""` // Priority of this subscription compared to others of the same permission type. 0-255 (higher = higher priority).
Title string `bun:",nullzero,unique"` // Moderator-set title for this list.
PermissionType DomainPermissionType `bun:",nullzero,notnull"` // Permission type of the subscription.
AsDraft *bool `bun:",nullzero,notnull,default:true"` // Create domain permission entries resulting from this subscription as drafts.
AdoptOrphans *bool `bun:",nullzero,notnull,default:false"` // Adopt orphaned domain permissions present in this subscription's entries.
CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this subscription.
CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID.
URI string `bun:",nullzero,notnull,unique"` // URI of the domain permission list.
ContentType DomainPermSubContentType `bun:",nullzero,notnull"` // Content type to expect from the URI.
FetchUsername string `bun:",nullzero"` // Username to send when doing a GET of URI using basic auth.
FetchPassword string `bun:",nullzero"` // Password to send when doing a GET of URI using basic auth.
FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // Time when fetch of URI was last attempted.
SuccessfullyFetchedAt time.Time `bun:"type:timestamptz,nullzero"` // Time when the domain permission list was last *successfuly* fetched, to be transmitted as If-Modified-Since header.
ETag string `bun:"etag,nullzero"` // Etag last received from the server (if any) on successful fetch.
Error string `bun:",nullzero"` // If latest fetch attempt errored, this field stores the error message. Cleared on latest successful fetch.
}
type DomainPermSubContentType enumType
const (
DomainPermSubContentTypeUnknown DomainPermSubContentType = 0 // ???
DomainPermSubContentTypeCSV DomainPermSubContentType = 1 // text/csv
DomainPermSubContentTypeJSON DomainPermSubContentType = 2 // application/json
DomainPermSubContentTypePlain DomainPermSubContentType = 3 // text/plain
)
func (p DomainPermSubContentType) String() string {
switch p {
case DomainPermSubContentTypeCSV:
return "text/csv"
case DomainPermSubContentTypeJSON:
return "application/json"
case DomainPermSubContentTypePlain:
return "text/plain"
default:
panic("unknown content type")
}
}
func NewDomainPermSubContentType(in string) DomainPermSubContentType {
switch in {
case "text/csv":
return DomainPermSubContentTypeCSV
case "application/json":
return DomainPermSubContentTypeJSON
case "text/plain":
return DomainPermSubContentTypePlain
default:
return DomainPermSubContentTypeUnknown
}
}

View file

@ -83,3 +83,12 @@ func NewRandomULID() (string, error) {
}
return newUlid.String(), nil
}
func TimeFromULID(id string) (time.Time, error) {
parsed, err := ulid.ParseStrict(id)
if err != nil {
return time.Time{}, err
}
return ulid.Time(parsed.Time()), nil
}

View file

@ -0,0 +1,285 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package admin
import (
"context"
"errors"
"fmt"
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/paging"
)
// DomainPermissionSubscriptionGet returns one
// domain permission subscription with the given id.
func (p *Processor) DomainPermissionSubscriptionGet(
ctx context.Context,
id string,
) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
permSub, err := p.state.DB.GetDomainPermissionSubscriptionByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission subscription %s: %w", id, err)
return nil, gtserror.NewErrorInternalError(err)
}
if permSub == nil {
err := fmt.Errorf("domain permission subscription %s not found", id)
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
return p.apiDomainPermSub(ctx, permSub)
}
// DomainPermissionSubscriptionsGet returns a page of
// DomainPermissionSubscriptions with the given parameters.
func (p *Processor) DomainPermissionSubscriptionsGet(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
page *paging.Page,
) (*apimodel.PageableResponse, gtserror.WithCode) {
permSubs, err := p.state.DB.GetDomainPermissionSubscriptions(
ctx,
permType,
page,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
count := len(permSubs)
if count == 0 {
return paging.EmptyResponse(), nil
}
// Get the lowest and highest
// ID values, used for paging.
lo := permSubs[count-1].ID
hi := permSubs[0].ID
// Convert each perm sub to API model.
items := make([]any, len(permSubs))
for i, permSub := range permSubs {
apiPermSub, err := p.converter.DomainPermSubToAPIDomainPermSub(ctx, permSub)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
items[i] = apiPermSub
}
// Assemble next/prev page queries.
query := make(url.Values, 1)
if permType != gtsmodel.DomainPermissionUnknown {
query.Set(apiutil.DomainPermissionPermTypeKey, permType.String())
}
return paging.PackageResponse(paging.ResponseParams{
Items: items,
Path: "/api/v1/admin/domain_permission_subscriptions",
Next: page.Next(lo, hi),
Prev: page.Prev(lo, hi),
Query: query,
}), nil
}
// DomainPermissionSubscriptionsGetByPriority returns all domain permission
// subscriptions of the given permission type, in descending priority order.
func (p *Processor) DomainPermissionSubscriptionsGetByPriority(
ctx context.Context,
permType gtsmodel.DomainPermissionType,
) ([]*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
permSubs, err := p.state.DB.GetDomainPermissionSubscriptionsByPriority(
ctx,
permType,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Convert each perm sub to API model.
items := make([]*apimodel.DomainPermissionSubscription, len(permSubs))
for i, permSub := range permSubs {
apiPermSub, err := p.converter.DomainPermSubToAPIDomainPermSub(ctx, permSub)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
items[i] = apiPermSub
}
return items, nil
}
func (p *Processor) DomainPermissionSubscriptionCreate(
ctx context.Context,
acct *gtsmodel.Account,
priority uint8,
title string,
uri string,
contentType gtsmodel.DomainPermSubContentType,
permType gtsmodel.DomainPermissionType,
asDraft bool,
fetchUsername string,
fetchPassword string,
) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
permSub := &gtsmodel.DomainPermissionSubscription{
ID: id.NewULID(),
Priority: priority,
Title: title,
PermissionType: permType,
AsDraft: &asDraft,
CreatedByAccountID: acct.ID,
CreatedByAccount: acct,
URI: uri,
ContentType: contentType,
FetchUsername: fetchUsername,
FetchPassword: fetchPassword,
}
err := p.state.DB.PutDomainPermissionSubscription(ctx, permSub)
if err != nil {
if errors.Is(err, db.ErrAlreadyExists) {
// Unique constraint conflict.
const errText = "domain permission subscription with given URI or title already exists"
return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
}
// Real database error.
err := gtserror.Newf("db error putting domain permission subscription: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return p.apiDomainPermSub(ctx, permSub)
}
func (p *Processor) DomainPermissionSubscriptionUpdate(
ctx context.Context,
id string,
priority *uint8,
title *string,
uri *string,
contentType *gtsmodel.DomainPermSubContentType,
asDraft *bool,
adoptOrphans *bool,
fetchUsername *string,
fetchPassword *string,
) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
permSub, err := p.state.DB.GetDomainPermissionSubscriptionByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission subscription %s: %w", id, err)
return nil, gtserror.NewErrorInternalError(err)
}
if permSub == nil {
err := fmt.Errorf("domain permission subscription %s not found", id)
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
columns := make([]string, 0, 7)
if priority != nil {
permSub.Priority = *priority
columns = append(columns, "priority")
}
if title != nil {
permSub.Title = *title
columns = append(columns, "title")
}
if uri != nil {
permSub.URI = *uri
columns = append(columns, "uri")
}
if contentType != nil {
permSub.ContentType = *contentType
columns = append(columns, "content_type")
}
if asDraft != nil {
permSub.AsDraft = asDraft
columns = append(columns, "as_draft")
}
if adoptOrphans != nil {
permSub.AdoptOrphans = adoptOrphans
columns = append(columns, "adopt_orphans")
}
if fetchPassword != nil {
permSub.FetchPassword = *fetchPassword
columns = append(columns, "fetch_password")
}
if fetchUsername != nil {
permSub.FetchUsername = *fetchUsername
columns = append(columns, "fetch_username")
}
err = p.state.DB.UpdateDomainPermissionSubscription(ctx, permSub, columns...)
if err != nil {
if errors.Is(err, db.ErrAlreadyExists) {
// Unique constraint conflict.
const errText = "domain permission subscription with given URI or title already exists"
return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
}
// Real database error.
err := gtserror.Newf("db error updating domain permission subscription: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return p.apiDomainPermSub(ctx, permSub)
}
func (p *Processor) DomainPermissionSubscriptionRemove(
ctx context.Context,
id string,
removeChildren bool,
) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
permSub, err := p.state.DB.GetDomainPermissionSubscriptionByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting domain permission subscription %s: %w", id, err)
return nil, gtserror.NewErrorInternalError(err)
}
if permSub == nil {
err := fmt.Errorf("domain permission subscription %s not found", id)
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
// TODO in next PR: if removeChildren, then remove all
// domain permissions that are children of this domain
// permission subscription. If not removeChildren, then
// just unlink them by clearing their subscription ID.
// For now just delete the domain permission subscription.
if err := p.state.DB.DeleteDomainPermissionSubscription(ctx, id); err != nil {
err := gtserror.Newf("db error deleting domain permission subscription: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return p.apiDomainPermSub(ctx, permSub)
}

View file

@ -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
}

View file

@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/language"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
@ -2130,6 +2131,60 @@ func (c *Converter) DomainPermToAPIDomainPerm(
return domainPerm, nil
}
func (c *Converter) DomainPermSubToAPIDomainPermSub(
ctx context.Context,
d *gtsmodel.DomainPermissionSubscription,
) (*apimodel.DomainPermissionSubscription, error) {
createdAt, err := id.TimeFromULID(d.ID)
if err != nil {
return nil, gtserror.Newf("error converting id to time: %w", err)
}
// URI may be in Punycode,
// de-punify it just in case.
uri, err := util.DePunify(d.URI)
if err != nil {
return nil, gtserror.Newf("error de-punifying URI %s: %w", d.URI, err)
}
var (
fetchedAt string
successfullyFetchedAt string
)
if !d.FetchedAt.IsZero() {
fetchedAt = util.FormatISO8601(d.FetchedAt)
}
if !d.SuccessfullyFetchedAt.IsZero() {
successfullyFetchedAt = util.FormatISO8601(d.SuccessfullyFetchedAt)
}
count, err := c.state.DB.CountDomainPermissionSubscriptionPerms(ctx, d.ID)
if err != nil {
return nil, gtserror.Newf("error counting perm sub perms: %w", err)
}
return &apimodel.DomainPermissionSubscription{
ID: d.ID,
Priority: d.Priority,
Title: d.Title,
PermissionType: d.PermissionType.String(),
AsDraft: *d.AsDraft,
AdoptOrphans: *d.AdoptOrphans,
CreatedBy: d.CreatedByAccountID,
CreatedAt: util.FormatISO8601(createdAt),
URI: uri,
ContentType: d.ContentType.String(),
FetchUsername: d.FetchUsername,
FetchPassword: d.FetchPassword,
FetchedAt: fetchedAt,
SuccessfullyFetchedAt: successfullyFetchedAt,
Error: d.Error,
Count: uint64(count), // #nosec G115 -- Don't care about overflow here.
}, nil
}
// ReportToAPIReport converts a gts model report into an api model report, for serving at /api/v1/reports
func (c *Converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (*apimodel.Report, error) {
report := &apimodel.Report{