diff --git a/internal/api/client/admin/domainpermissiondraft.go b/internal/api/client/admin/domainpermissiondraft.go index 548f966ef..085560b93 100644 --- a/internal/api/client/admin/domainpermissiondraft.go +++ b/internal/api/client/admin/domainpermissiondraft.go @@ -76,7 +76,8 @@ func (m *Module) deleteDomainPermissionDraft( apiutil.JSON(c, http.StatusOK, domainPerm) } -// getDomainPermissionDraft gets a single domain permission (block or allow). +// getDomainPermissionDraft gets a single +// domain permission draft (block or allow). func (m *Module) getDomainPermissionDraft( c *gin.Context, permType gtsmodel.DomainPermissionType, @@ -104,17 +105,10 @@ func (m *Module) getDomainPermissionDraft( return } - export, errWithCode := apiutil.ParseDomainPermissionDraftExport(c.Query(apiutil.DomainPermissionDraftExportKey), false) - if errWithCode != nil { - apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) - return - } - domainPerm, errWithCode := m.processor.Admin().DomainPermissionDraftGet( c.Request.Context(), permType, domainPermID, - export, ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 09e505ff5..199e73c80 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -74,6 +74,9 @@ func (c *Caches) Init() { c.initConversationLastStatusIDs() c.initDomainAllow() c.initDomainBlock() + c.initDomainPermissionDraft() + c.initDomainPermissionSubscription() + c.initDomainPermissionIgnore() c.initEmoji() c.initEmojiCategory() c.initFilter() diff --git a/internal/cache/db.go b/internal/cache/db.go index 64a3ab120..f31a19e3b 100644 --- a/internal/cache/db.go +++ b/internal/cache/db.go @@ -73,6 +73,9 @@ type DBCaches struct { // DomainPermissionSubscription provides access to the domain permission subscription database cache. DomainPermissionSubscription StructCache[*gtsmodel.DomainPermissionSubscription] + // DomainPermissionIgnore provides access to the domain permission ignore database cache. + DomainPermissionIgnore *domain.Cache + // Emoji provides access to the gtsmodel Emoji database cache. Emoji StructCache[*gtsmodel.Emoji] @@ -617,6 +620,10 @@ func (c *Caches) initDomainPermissionSubscription() { }) } +func (c *Caches) initDomainPermissionIgnore() { + c.DB.DomainPermissionIgnore = new(domain.Cache) +} + func (c *Caches) initEmoji() { // Calculate maximum cache size. cap := calculateResultCacheMax( diff --git a/internal/db/bundb/domain.go b/internal/db/bundb/domain.go index 1350c115c..fe98fc3fb 100644 --- a/internal/db/bundb/domain.go +++ b/internal/db/bundb/domain.go @@ -23,6 +23,7 @@ "net/url" "slices" + "github.com/miekg/dns" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" @@ -593,15 +594,15 @@ func (d *domainDB) getDomainPermissionSubscription( keyParts ...any, ) (*gtsmodel.DomainPermissionSubscription, error) { // Fetch perm subscription from database cache with loader callback. - permDraft, err := d.state.Caches.DB.DomainPermissionSubscription.LoadOne( + permSub, err := d.state.Caches.DB.DomainPermissionSubscription.LoadOne( lookup, // Only called if not cached. func() (*gtsmodel.DomainPermissionSubscription, error) { - var permDraft gtsmodel.DomainPermissionSubscription - if err := dbQuery(&permDraft); err != nil { + var permSub gtsmodel.DomainPermissionSubscription + if err := dbQuery(&permSub); err != nil { return nil, err } - return &permDraft, nil + return &permSub, nil }, keyParts..., ) @@ -611,21 +612,21 @@ func() (*gtsmodel.DomainPermissionSubscription, error) { if gtscontext.Barebones(ctx) { // No need to fully populate. - return permDraft, nil + return permSub, nil } - if permDraft.CreatedByAccount == nil { + if permSub.CreatedByAccount == nil { // Not set, fetch from database. - permDraft.CreatedByAccount, err = d.state.DB.GetAccountByID( + permSub.CreatedByAccount, err = d.state.DB.GetAccountByID( gtscontext.SetBarebones(ctx), - permDraft.CreatedByAccountID, + permSub.CreatedByAccountID, ) if err != nil { return nil, gtserror.Newf("error populating created by account: %w", err) } } - return permDraft, nil + return permSub, nil } func (d *domainDB) GetDomainPermissionSubscriptionByID( @@ -635,10 +636,10 @@ func (d *domainDB) GetDomainPermissionSubscriptionByID( return d.getDomainPermissionSubscription( ctx, "ID", - func(permDraft *gtsmodel.DomainPermissionSubscription) error { + func(permSub *gtsmodel.DomainPermissionSubscription) error { return d.db. NewSelect(). - Model(permDraft). + Model(permSub). Where("? = ?", bun.Ident("domain_permission_subscription.id"), id). Scan(ctx) }, @@ -743,14 +744,14 @@ func (d *domainDB) GetDomainPermissionSubscriptions( // Allocate return slice (will be at most len permSubIDs). permSubs := make([]*gtsmodel.DomainPermissionSubscription, 0, len(permSubIDs)) for _, id := range permSubIDs { - permDraft, err := d.GetDomainPermissionSubscriptionByID(ctx, id) + 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, permDraft) + permSubs = append(permSubs, permSub) } return permSubs, nil @@ -799,3 +800,235 @@ func (d *domainDB) DeleteDomainPermissionSubscription( return nil } + +func (d *domainDB) PutDomainPermissionIgnore( + ctx context.Context, + ignore *gtsmodel.DomainPermissionIgnore, +) error { + // Normalize the domain as punycode + var err error + ignore.Domain, err = util.Punify(ignore.Domain) + if err != nil { + return err + } + + // Attempt to store domain perm ignore in DB + if _, err := d.db.NewInsert(). + Model(ignore). + Exec(ctx); err != nil { + return err + } + + // Clear the domain perm ignore cache (for later reload) + d.state.Caches.DB.DomainPermissionIgnore.Clear() + + return nil +} + +func (d *domainDB) IsDomainPermissionIgnored(ctx context.Context, domain string) (bool, error) { + // Normalize the domain as punycode + domain, err := util.Punify(domain) + if err != nil { + return false, err + } + + // Check if our host and given domain are equal + // or part of the same second-level domain; we + // always ignore such perms as creating blocks + // or allows in such cases may break things. + if dns.CompareDomainName(domain, config.GetHost()) >= 2 { + return true, nil + } + + // Func to scan list of all + // ignored domain perms from DB. + loadF := func() ([]string, error) { + var domains []string + + if err := d.db. + NewSelect(). + Table("domain_ignores"). + Column("domain"). + Scan(ctx, &domains); err != nil { + return nil, err + } + + return domains, nil + } + + // Check the cache for a domain perm ignore, + // hydrating the cache with loadF if necessary. + return d.state.Caches.DB.DomainPermissionIgnore.Matches(domain, loadF) +} + +func (d *domainDB) GetDomainPermissionIgnoreByID( + ctx context.Context, + id string, +) (*gtsmodel.DomainPermissionIgnore, error) { + ignore := new(gtsmodel.DomainPermissionIgnore) + + q := d.db. + NewSelect(). + Model(ignore). + Where("? = ?", bun.Ident("domain_permission_ignore.id"), id) + if err := q.Scan(ctx); err != nil { + return nil, err + } + + if gtscontext.Barebones(ctx) { + // No need to fully populate. + return ignore, nil + } + + if ignore.CreatedByAccount == nil { + // Not set, fetch from database. + var err error + ignore.CreatedByAccount, err = d.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + ignore.CreatedByAccountID, + ) + if err != nil { + return nil, gtserror.Newf("error populating created by account: %w", err) + } + } + + return ignore, nil +} + +func (d *domainDB) GetDomainPermissionIgnores( + ctx context.Context, + permType *gtsmodel.DomainPermissionType, + page *paging.Page, +) ( + []*gtsmodel.DomainPermissionIgnore, + error, +) { + var ( + // Get paging params. + minID = page.GetMin() + maxID = page.GetMax() + limit = page.GetLimit() + order = page.GetOrder() + + // Make educated guess for slice size + ignoreIDs = make([]string, 0, limit) + ) + + q := d.db. + NewSelect(). + TableExpr( + "? AS ?", + bun.Ident("domain_permission_ignores"), + bun.Ident("domain_permission_ignore"), + ). + // Select only IDs from table + Column("domain_permission_ignore.id") + + // Return only items with id + // lower than provided maxID. + if maxID != "" { + q = q.Where( + "? < ?", + bun.Ident("domain_permission_ignore.id"), + maxID, + ) + } + + // Return only items with id + // greater than provided minID. + if minID != "" { + q = q.Where( + "? > ?", + bun.Ident("domain_permission_ignore.id"), + minID, + ) + } + + // Return only items with + // given subscription ID. + if permType != nil { + q = q.Where( + "? = ?", + bun.Ident("domain_permission_ignore.permission_type"), + *permType, + ) + } + + if limit > 0 { + // Limit amount of + // items returned. + q = q.Limit(limit) + } + + if order == paging.OrderAscending { + // Page up. + q = q.OrderExpr( + "? ASC", + bun.Ident("domain_permission_ignore.id"), + ) + } else { + // Page down. + q = q.OrderExpr( + "? DESC", + bun.Ident("domain_permission_ignore.id"), + ) + } + + if err := q.Scan(ctx, &ignoreIDs); err != nil { + return nil, err + } + + // Catch case of no items early + if len(ignoreIDs) == 0 { + return nil, db.ErrNoEntries + } + + // If we're paging up, we still want items + // to be sorted by ID desc, so reverse slice. + if order == paging.OrderAscending { + slices.Reverse(ignoreIDs) + } + + // Allocate return slice (will be at most len permSubIDs). + ignores := make([]*gtsmodel.DomainPermissionIgnore, 0, len(ignoreIDs)) + for _, id := range ignoreIDs { + ignore, err := d.GetDomainPermissionIgnoreByID(ctx, id) + if err != nil { + log.Errorf(ctx, "error getting domain permission ignore %q: %v", id, err) + continue + } + + // Append to return slice + ignores = append(ignores, ignore) + } + + return ignores, nil +} + +func (d *domainDB) DeleteDomainPermissionIgnore( + ctx context.Context, + id string, +) error { + // Delete the permSub from DB. + q := d.db.NewDelete(). + TableExpr( + "? AS ?", + bun.Ident("domain_permission_ignores"), + bun.Ident("domain_permission_ignore"), + ). + Where( + "? = ?", + bun.Ident("domain_permission_ignore.id"), + id, + ) + + _, err := q.Exec(ctx) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } + + // Clear the domain perm ignore cache (for later reload) + d.state.Caches.DB.DomainPermissionIgnore.Clear() + + return nil +} diff --git a/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go index 929529ced..3a7db3dc6 100644 --- a/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go +++ b/internal/db/bundb/migrations/20241022153016_domain_permission_subscriptions.go @@ -46,6 +46,15 @@ func init() { return err } + // Create `domain_permission_ignores`. + if _, err := tx. + NewCreateTable(). + Model((*gtsmodel.DomainPermissionIgnore)(nil)). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + // Create indexes. Indices. Indie sexes. for table, indexes := range map[string]map[string][]string{ "domain_permission_drafts": { diff --git a/internal/gtsmodel/domainpermissiondraft.go b/internal/gtsmodel/domainpermissiondraft.go index aa066a5db..b40424553 100644 --- a/internal/gtsmodel/domainpermissiondraft.go +++ b/internal/gtsmodel/domainpermissiondraft.go @@ -27,8 +27,52 @@ type DomainPermissionDraft struct { Domain string `bun:",nullzero,notnull,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"` // Domain to block or allow. Eg. 'whatever.com'. CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this subscription. CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID. - PrivateComment string `bun:""` // Private comment on this perm, viewable to admins. - PublicComment string `bun:""` // Public comment on this perm, viewable (optionally) by everyone. + PrivateComment string `bun:",nullzero"` // Private comment on this perm, viewable to admins. + PublicComment string `bun:",nullzero"` // Public comment on this perm, viewable (optionally) by everyone. Obfuscate *bool `bun:",nullzero,notnull,default:false"` // Obfuscate domain name when displaying it publicly. SubscriptionID string `bun:"type:CHAR(26),nullzero,unique:domain_permission_drafts_permission_type_domain_subscription_id_uniq"` // ID of the subscription that created this draft, if any. } + +func (d *DomainPermissionDraft) GetID() string { + return d.ID +} + +func (d *DomainPermissionDraft) GetCreatedAt() time.Time { + return d.CreatedAt +} + +func (d *DomainPermissionDraft) GetUpdatedAt() time.Time { + return d.UpdatedAt +} + +func (d *DomainPermissionDraft) GetDomain() string { + return d.Domain +} + +func (d *DomainPermissionDraft) GetCreatedByAccountID() string { + return d.CreatedByAccountID +} + +func (d *DomainPermissionDraft) GetCreatedByAccount() *Account { + return d.CreatedByAccount +} + +func (d *DomainPermissionDraft) GetPrivateComment() string { + return d.PrivateComment +} + +func (d *DomainPermissionDraft) GetPublicComment() string { + return d.PublicComment +} + +func (d *DomainPermissionDraft) GetObfuscate() *bool { + return d.Obfuscate +} + +func (d *DomainPermissionDraft) GetSubscriptionID() string { + return d.SubscriptionID +} + +func (d *DomainPermissionDraft) GetType() DomainPermissionType { + return DomainPermissionBlock +} diff --git a/internal/gtsmodel/domainpermissionignore.go b/internal/gtsmodel/domainpermissionignore.go new file mode 100644 index 000000000..727bccdf3 --- /dev/null +++ b/internal/gtsmodel/domainpermissionignore.go @@ -0,0 +1,32 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import "time" + +// DomainPermissionIgnore represents one domain that should be ignored +// when domain permission (ignores) are created from subscriptions. +type DomainPermissionIgnore struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Time when this item was created. + PermissionType DomainPermissionType `bun:",notnull,unique:domain_permission_ignores_permission_type_domain_uniq"` // Permission type of the ignore. + Domain string `bun:",nullzero,notnull,unique:domain_permission_ignores_permission_type_domain_uniq"` // Domain to ignore. Eg. 'whatever.com'. + CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this ignore. + CreatedByAccount *Account `bun:"-"` // Account corresponding to createdByAccountID. + PrivateComment string `bun:",nullzero"` // Private comment on this ignore, viewable to admins. +} diff --git a/internal/processing/admin/domainpermission.go b/internal/processing/admin/domainpermission.go index bedaf6a11..9fce72363 100644 --- a/internal/processing/admin/domainpermission.go +++ b/internal/processing/admin/domainpermission.go @@ -32,8 +32,7 @@ ) // apiDomainPerm is a cheeky shortcut for returning -// the API version of the given domain permission -// (*gtsmodel.DomainBlock or *gtsmodel.DomainAllow), +// the API version of the given domain permission, // or an appropriate error if something goes wrong. func (p *Processor) apiDomainPerm( ctx context.Context, @@ -333,3 +332,28 @@ func (p *Processor) DomainPermissionGet( return p.apiDomainPerm(ctx, domainPerm, export) } + +func (p *Processor) DomainPermissionDraftGet( + ctx context.Context, + permissionType gtsmodel.DomainPermissionType, + id string, +) (*apimodel.DomainPermission, gtserror.WithCode) { + permDraft, err := p.state.DB.GetDomainPermissionDraftByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = gtserror.Newf( + "db error getting domain %s draft %s: %w", + permissionType.String(), id, err, + ) + return nil, gtserror.NewErrorInternalError(err) + } + + if permDraft == nil { + err = fmt.Errorf( + "no domain %s draft exists with id %s", + permissionType.String(), id, + ) + return nil, gtserror.NewErrorNotFound(err, err.Error()) + } + + return p.apiDomainPerm(ctx, permDraft, false) +}