[feature] Fetch + create domain permissions from subscriptions nightly (#3635)

* peepeepoopoo

* test domain perm subs

* swagger

* envparsing

* dries your wets

* start on docs

* finish up docs

* copy paste errors

* rename actions package

* rename force -> skipCache

* move obfuscate parse nearer to where err is checked

* make higherPrios a simple slice

* don't use receiver for permsFrom funcs

* add more context to error logs

* defer finished log

* use switch for permType instead of if/else

* thanks linter, love you <3

* validate csv headers before full read

* use bufio scanner
This commit is contained in:
tobi
2025-01-08 11:29:40 +01:00
committed by GitHub
parent c013892ca2
commit 451803b230
95 changed files with 3320 additions and 626 deletions

View File

@@ -53,7 +53,7 @@ func (suite *AccountTestSuite) TestAccountActionSuspend() {
// Wait for action to finish.
if !testrig.WaitFor(func() bool {
return suite.adminProcessor.Actions().TotalRunning() == 0
return suite.state.AdminActions.TotalRunning() == 0
}) {
suite.FailNow("timed out waiting for admin action(s) to finish")
}

View File

@@ -68,7 +68,7 @@ func (p *Processor) accountActionSuspend(
) (string, gtserror.WithCode) {
actionID := id.NewULID()
errWithCode := p.actions.Run(
errWithCode := p.state.AdminActions.Run(
ctx,
&gtsmodel.AdminAction{
ID: actionID,

View File

@@ -1,170 +0,0 @@
// 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"
"slices"
"sync"
"time"
"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/state"
)
func errActionConflict(action *gtsmodel.AdminAction) gtserror.WithCode {
err := gtserror.NewfAt(
4, // Include caller's function name.
"an action (%s) is currently running (duration %s) which conflicts with the attempted action",
action.Key(), time.Since(action.CreatedAt),
)
const help = "wait until this action is complete and try again"
return gtserror.NewErrorConflict(err, err.Error(), help)
}
type Actions struct {
r map[string]*gtsmodel.AdminAction
state *state.State
// Not embedded struct,
// to shield from access
// by outside packages.
m sync.Mutex
}
// Run runs the given admin action by executing the supplied function.
//
// Run handles locking, action insertion and updating, so you don't have to!
//
// If an action is already running which overlaps/conflicts with the
// given action, an ErrorWithCode 409 will be returned.
//
// If execution of the provided function returns errors, the errors
// will be updated on the provided admin action in the database.
func (a *Actions) Run(
ctx context.Context,
action *gtsmodel.AdminAction,
f func(context.Context) gtserror.MultiError,
) gtserror.WithCode {
actionKey := action.Key()
// LOCK THE MAP HERE, since we're
// going to do some operations on it.
a.m.Lock()
// Bail if an action with
// this key is already running.
running, ok := a.r[actionKey]
if ok {
a.m.Unlock()
return errActionConflict(running)
}
// Action with this key not
// yet running, create it.
if err := a.state.DB.PutAdminAction(ctx, action); err != nil {
err = gtserror.Newf("db error putting admin action %s: %w", actionKey, err)
// Don't store in map
// if there's an error.
a.m.Unlock()
return gtserror.NewErrorInternalError(err)
}
// Action was inserted,
// store in map.
a.r[actionKey] = action
// UNLOCK THE MAP HERE, since
// we're done modifying it for now.
a.m.Unlock()
go func() {
// Use a background context with existing values.
ctx = gtscontext.WithValues(context.Background(), ctx)
// Run the thing and collect errors.
if errs := f(ctx); errs != nil {
action.Errors = make([]string, 0, len(errs))
for _, err := range errs {
action.Errors = append(action.Errors, err.Error())
}
}
// Action is no longer running:
// remove from running map.
a.m.Lock()
delete(a.r, actionKey)
a.m.Unlock()
// Mark as completed in the db,
// storing errors for later review.
action.CompletedAt = time.Now()
if err := a.state.DB.UpdateAdminAction(ctx, action, "completed_at", "errors"); err != nil {
log.Errorf(ctx, "db error marking action %s as completed: %q", actionKey, err)
}
}()
return nil
}
// GetRunning sounds like a threat, but it actually just
// returns all of the currently running actions held by
// the Actions struct, ordered by ID descending.
func (a *Actions) GetRunning() []*gtsmodel.AdminAction {
a.m.Lock()
defer a.m.Unlock()
// Assemble all currently running actions.
running := make([]*gtsmodel.AdminAction, 0, len(a.r))
for _, action := range a.r {
running = append(running, action)
}
// Order by ID descending (creation date).
slices.SortFunc(
running,
func(a *gtsmodel.AdminAction, b *gtsmodel.AdminAction) int {
const k = -1
switch {
case a.ID > b.ID:
return +k
case a.ID < b.ID:
return -k
default:
return 0
}
},
)
return running
}
// TotalRunning is a sequel to the classic
// 1972 environmental-themed science fiction
// film Silent Running, starring Bruce Dern.
func (a *Actions) TotalRunning() int {
a.m.Lock()
defer a.m.Unlock()
return len(a.r)
}

View File

@@ -1,162 +0,0 @@
// 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_test
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/suite"
"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/testrig"
)
type ActionsTestSuite struct {
AdminStandardTestSuite
}
func (suite *ActionsTestSuite) TestActionOverlap() {
ctx := context.Background()
// Suspend account.
action1 := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryAccount,
TargetID: "01H90S1CXQ97J9625C5YBXZWGT",
Type: gtsmodel.AdminActionSuspend,
AccountID: "01H90S1ZZXP4N74H4A9RVW1MRP",
}
key1 := action1.Key()
suite.Equal("account/01H90S1CXQ97J9625C5YBXZWGT", key1)
// Unsuspend account.
action2 := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryAccount,
TargetID: "01H90S1CXQ97J9625C5YBXZWGT",
Type: gtsmodel.AdminActionUnsuspend,
AccountID: "01H90S1ZZXP4N74H4A9RVW1MRP",
}
key2 := action2.Key()
suite.Equal("account/01H90S1CXQ97J9625C5YBXZWGT", key2)
errWithCode := suite.adminProcessor.Actions().Run(
ctx,
action1,
func(ctx context.Context) gtserror.MultiError {
// Noop, just sleep (mood).
time.Sleep(3 * time.Second)
return nil
},
)
suite.NoError(errWithCode)
// While first action is sleeping, try to
// process another with the same key.
errWithCode = suite.adminProcessor.Actions().Run(
ctx,
action2,
func(ctx context.Context) gtserror.MultiError {
return nil
},
)
if errWithCode == nil {
suite.FailNow("expected error with code, but error was nil")
}
// Code should be 409.
suite.Equal(http.StatusConflict, errWithCode.Code())
// Wait for action to finish.
if !testrig.WaitFor(func() bool {
return suite.adminProcessor.Actions().TotalRunning() == 0
}) {
suite.FailNow("timed out waiting for admin action(s) to finish")
}
// Try again.
errWithCode = suite.adminProcessor.Actions().Run(
ctx,
action2,
func(ctx context.Context) gtserror.MultiError {
return nil
},
)
suite.NoError(errWithCode)
// Wait for action to finish.
if !testrig.WaitFor(func() bool {
return suite.adminProcessor.Actions().TotalRunning() == 0
}) {
suite.FailNow("timed out waiting for admin action(s) to finish")
}
}
func (suite *ActionsTestSuite) TestActionWithErrors() {
ctx := context.Background()
// Suspend a domain.
action := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: "example.org",
Type: gtsmodel.AdminActionSuspend,
AccountID: "01H90S1ZZXP4N74H4A9RVW1MRP",
}
errWithCode := suite.adminProcessor.Actions().Run(
ctx,
action,
func(ctx context.Context) gtserror.MultiError {
// Noop, just return some errs.
return gtserror.MultiError{
db.ErrNoEntries,
errors.New("fucky wucky"),
}
},
)
suite.NoError(errWithCode)
// Wait for action to finish.
if !testrig.WaitFor(func() bool {
return suite.adminProcessor.Actions().TotalRunning() == 0
}) {
suite.FailNow("timed out waiting for admin action(s) to finish")
}
// Get action from the db.
dbAction, err := suite.db.GetAdminAction(ctx, action.ID)
if err != nil {
suite.FailNow(err.Error())
}
suite.EqualValues([]string{
"sql: no rows in result set",
"fucky wucky",
}, dbAction.Errors)
}
func TestActionsTestSuite(t *testing.T) {
suite.Run(t, new(ActionsTestSuite))
}

View File

@@ -21,10 +21,10 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/subscriptions"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
@@ -33,21 +33,14 @@ type Processor struct {
// common processor logic
c *common.Processor
state *state.State
cleaner *cleaner.Cleaner
converter *typeutils.Converter
federator *federation.Federator
media *media.Manager
transport transport.Controller
email email.Sender
// admin Actions currently
// undergoing processing
actions *Actions
}
func (p *Processor) Actions() *Actions {
return p.actions
state *state.State
cleaner *cleaner.Cleaner
subscriptions *subscriptions.Subscriptions
converter *typeutils.Converter
federator *federation.Federator
media *media.Manager
transport transport.Controller
email email.Sender
}
// New returns a new admin processor.
@@ -55,6 +48,7 @@ func New(
common *common.Processor,
state *state.State,
cleaner *cleaner.Cleaner,
subscriptions *subscriptions.Subscriptions,
federator *federation.Federator,
converter *typeutils.Converter,
mediaManager *media.Manager,
@@ -62,17 +56,14 @@ func New(
emailSender email.Sender,
) Processor {
return Processor{
c: common,
state: state,
cleaner: cleaner,
converter: converter,
federator: federator,
media: mediaManager,
transport: transportController,
email: emailSender,
actions: &Actions{
r: make(map[string]*gtsmodel.AdminAction),
state: state,
},
c: common,
state: state,
cleaner: cleaner,
subscriptions: subscriptions,
converter: converter,
federator: federator,
media: mediaManager,
transport: transportController,
email: emailSender,
}
}

View File

@@ -19,6 +19,7 @@ package admin_test
import (
"github.com/stretchr/testify/suite"
adminactions "github.com/superseriousbusiness/gotosocial/internal/admin"
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
@@ -33,6 +34,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/subscriptions"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
@@ -89,6 +91,7 @@ func (suite *AdminStandardTestSuite) SetupTest() {
suite.db = testrig.NewTestDB(&suite.state)
suite.state.DB = suite.db
suite.state.AdminActions = adminactions.New(suite.state.DB, &suite.state.Workers)
suite.tc = typeutils.NewConverter(&suite.state)
testrig.StartTimelines(
@@ -109,6 +112,7 @@ func (suite *AdminStandardTestSuite) SetupTest() {
suite.processor = processing.NewProcessor(
cleaner.New(&suite.state),
subscriptions.New(&suite.state, suite.transportController, suite.tc),
suite.tc,
suite.federator,
suite.oauthServer,

View File

@@ -22,14 +22,11 @@ import (
"errors"
"fmt"
"codeberg.org/gruf/go-kv"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"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/log"
"github.com/superseriousbusiness/gotosocial/internal/text"
)
@@ -69,84 +66,30 @@ func (p *Processor) createDomainAllow(
}
}
actionID := id.NewULID()
// Run admin action to process
// side effects of allow.
action := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domainAllow.Domain,
Type: gtsmodel.AdminActionUnsuspend,
AccountID: adminAcct.ID,
}
// Process domain allow side
// effects asynchronously.
if errWithCode := p.actions.Run(
if errWithCode := p.state.AdminActions.Run(
ctx,
&gtsmodel.AdminAction{
ID: actionID,
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domain,
Type: gtsmodel.AdminActionSuspend,
AccountID: adminAcct.ID,
Text: domainAllow.PrivateComment,
},
func(ctx context.Context) gtserror.MultiError {
// Log start + finish.
l := log.WithFields(kv.Fields{
{"domain", domain},
{"actionID", actionID},
}...).WithContext(ctx)
l.Info("processing domain allow side effects")
defer func() { l.Info("finished processing domain allow side effects") }()
return p.domainAllowSideEffects(ctx, domainAllow)
},
action,
p.state.AdminActions.DomainAllowF(action.ID, domainAllow),
); errWithCode != nil {
return nil, actionID, errWithCode
return nil, action.ID, errWithCode
}
apiDomainAllow, errWithCode := p.apiDomainPerm(ctx, domainAllow, false)
if errWithCode != nil {
return nil, actionID, errWithCode
return nil, action.ID, errWithCode
}
return apiDomainAllow, actionID, nil
}
func (p *Processor) domainAllowSideEffects(
ctx context.Context,
allow *gtsmodel.DomainAllow,
) gtserror.MultiError {
if config.GetInstanceFederationMode() == config.InstanceFederationModeAllowlist {
// We're running in allowlist mode,
// so there are no side effects to
// process here.
return nil
}
// We're running in blocklist mode or
// some similar mode which necessitates
// domain allow side effects if a block
// was in place when the allow was created.
//
// So, check if there's a block.
block, err := p.state.DB.GetDomainBlock(ctx, allow.Domain)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
errs := gtserror.NewMultiError(1)
errs.Appendf("db error getting domain block %s: %w", allow.Domain, err)
return errs
}
if block == nil {
// No block?
// No problem!
return nil
}
// There was a block, over which the new
// allow ought to take precedence. To account
// for this, just run side effects as though
// the domain was being unblocked, while
// leaving the existing block in place.
//
// Any accounts that were suspended by
// the block will be unsuspended and be
// able to interact with the instance again.
return p.domainUnblockSideEffects(ctx, block)
return apiDomainAllow, action.ID, nil
}
func (p *Processor) deleteDomainAllow(
@@ -179,77 +122,23 @@ func (p *Processor) deleteDomainAllow(
return nil, "", gtserror.NewErrorInternalError(err)
}
actionID := id.NewULID()
// Run admin action to process
// side effects of unallow.
action := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domainAllow.Domain,
Type: gtsmodel.AdminActionUnsuspend,
AccountID: adminAcct.ID,
}
// Process domain unallow side
// effects asynchronously.
if errWithCode := p.actions.Run(
if errWithCode := p.state.AdminActions.Run(
ctx,
&gtsmodel.AdminAction{
ID: actionID,
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domainAllow.Domain,
Type: gtsmodel.AdminActionUnsuspend,
AccountID: adminAcct.ID,
},
func(ctx context.Context) gtserror.MultiError {
// Log start + finish.
l := log.WithFields(kv.Fields{
{"domain", domainAllow.Domain},
{"actionID", actionID},
}...).WithContext(ctx)
l.Info("processing domain unallow side effects")
defer func() { l.Info("finished processing domain unallow side effects") }()
return p.domainUnallowSideEffects(ctx, domainAllow)
},
action,
p.state.AdminActions.DomainUnallowF(action.ID, domainAllow),
); errWithCode != nil {
return nil, actionID, errWithCode
return nil, action.ID, errWithCode
}
return apiDomainAllow, actionID, nil
}
func (p *Processor) domainUnallowSideEffects(
ctx context.Context,
allow *gtsmodel.DomainAllow,
) gtserror.MultiError {
if config.GetInstanceFederationMode() == config.InstanceFederationModeAllowlist {
// We're running in allowlist mode,
// so there are no side effects to
// process here.
return nil
}
// We're running in blocklist mode or
// some similar mode which necessitates
// domain allow side effects if a block
// was in place when the allow was removed.
//
// So, check if there's a block.
block, err := p.state.DB.GetDomainBlock(ctx, allow.Domain)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
errs := gtserror.NewMultiError(1)
errs.Appendf("db error getting domain block %s: %w", allow.Domain, err)
return errs
}
if block == nil {
// No block?
// No problem!
return nil
}
// There was a block, over which the previous
// allow was taking precedence. Now that the
// allow has been removed, we should put the
// side effects of the block back in place.
//
// To do this, process the block side effects
// again as though the block were freshly
// created. This will mark all accounts from
// the blocked domain as suspended, and clean
// up their follows/following, media, etc.
return p.domainBlockSideEffects(ctx, block)
return apiDomainAllow, action.ID, nil
}

View File

@@ -21,18 +21,12 @@ import (
"context"
"errors"
"fmt"
"time"
"codeberg.org/gruf/go-kv"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"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/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text"
)
@@ -72,149 +66,31 @@ func (p *Processor) createDomainBlock(
}
}
actionID := id.NewULID()
// Run admin action to process
// side effects of block.
action := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domain,
Type: gtsmodel.AdminActionSuspend,
AccountID: adminAcct.ID,
Text: domainBlock.PrivateComment,
}
// Process domain block side
// effects asynchronously.
if errWithCode := p.actions.Run(
if errWithCode := p.state.AdminActions.Run(
ctx,
&gtsmodel.AdminAction{
ID: actionID,
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domain,
Type: gtsmodel.AdminActionSuspend,
AccountID: adminAcct.ID,
Text: domainBlock.PrivateComment,
},
func(ctx context.Context) gtserror.MultiError {
// Log start + finish.
l := log.WithFields(kv.Fields{
{"domain", domain},
{"actionID", actionID},
}...).WithContext(ctx)
skip, err := p.skipBlockSideEffects(ctx, domain)
if err != nil {
return err
}
if skip != "" {
l.Infof("skipping domain block side effects: %s", skip)
return nil
}
l.Info("processing domain block side effects")
defer func() { l.Info("finished processing domain block side effects") }()
return p.domainBlockSideEffects(ctx, domainBlock)
},
action,
p.state.AdminActions.DomainBlockF(action.ID, domainBlock),
); errWithCode != nil {
return nil, actionID, errWithCode
return nil, action.ID, errWithCode
}
apiDomainBlock, errWithCode := p.apiDomainPerm(ctx, domainBlock, false)
if errWithCode != nil {
return nil, actionID, errWithCode
return nil, action.ID, errWithCode
}
return apiDomainBlock, actionID, nil
}
// skipBlockSideEffects checks if side effects of block creation
// should be skipped for the given domain, taking account of
// instance federation mode, and existence of any allows
// which ought to "shield" this domain from being blocked.
//
// If the caller should skip, the returned string will be non-zero
// and will be set to a reason why side effects should be skipped.
//
// - blocklist mode + allow exists: "..." (skip)
// - blocklist mode + no allow: "" (don't skip)
// - allowlist mode + allow exists: "" (don't skip)
// - allowlist mode + no allow: "" (don't skip)
func (p *Processor) skipBlockSideEffects(
ctx context.Context,
domain string,
) (string, gtserror.MultiError) {
var (
skip string // Assume "" (don't skip).
errs gtserror.MultiError
)
// Never skip block side effects in allowlist mode.
fediMode := config.GetInstanceFederationMode()
if fediMode == config.InstanceFederationModeAllowlist {
return skip, errs
}
// We know we're in blocklist mode.
//
// We want to skip domain block side
// effects if an allow is already
// in place which overrides the block.
// Check if an explicit allow exists for this domain.
domainAllow, err := p.state.DB.GetDomainAllow(ctx, domain)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
errs.Appendf("error getting domain allow: %w", err)
return skip, errs
}
if domainAllow != nil {
skip = "running in blocklist mode, and an explicit allow exists for this domain"
return skip, errs
}
return skip, errs
}
// domainBlockSideEffects processes the side effects of a domain block:
//
// 1. Strip most info away from the instance entry for the domain.
// 2. Pass each account from the domain to the processor for deletion.
//
// It should be called asynchronously, since it can take a while when
// there are many accounts present on the given domain.
func (p *Processor) domainBlockSideEffects(
ctx context.Context,
block *gtsmodel.DomainBlock,
) gtserror.MultiError {
var errs gtserror.MultiError
// If we have an instance entry for this domain,
// update it with the new block ID and clear all fields
instance, err := p.state.DB.GetInstance(ctx, block.Domain)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
errs.Appendf("db error getting instance %s: %w", block.Domain, err)
return errs
}
if instance != nil {
// We had an entry for this domain.
columns := stubbifyInstance(instance, block.ID)
if err := p.state.DB.UpdateInstance(ctx, instance, columns...); err != nil {
errs.Appendf("db error updating instance: %w", err)
return errs
}
}
// For each account that belongs to this domain,
// process an account delete message to remove
// that account's posts, media, etc.
if err := p.rangeDomainAccounts(ctx, block.Domain, func(account *gtsmodel.Account) {
if err := p.state.Workers.Client.Process(ctx, &messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityDelete,
GTSModel: block,
Origin: account,
Target: account,
}); err != nil {
errs.Append(err)
}
}); err != nil {
errs.Appendf("db error ranging through accounts: %w", err)
}
return errs
return apiDomainBlock, action.ID, nil
}
func (p *Processor) deleteDomainBlock(
@@ -247,104 +123,23 @@ func (p *Processor) deleteDomainBlock(
return nil, "", gtserror.NewErrorInternalError(err)
}
actionID := id.NewULID()
// Run admin action to process
// side effects of unblock.
action := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domainBlock.Domain,
Type: gtsmodel.AdminActionUnsuspend,
AccountID: adminAcct.ID,
}
// Process domain unblock side
// effects asynchronously.
if errWithCode := p.actions.Run(
if errWithCode := p.state.AdminActions.Run(
ctx,
&gtsmodel.AdminAction{
ID: actionID,
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domainBlock.Domain,
Type: gtsmodel.AdminActionUnsuspend,
AccountID: adminAcct.ID,
},
func(ctx context.Context) gtserror.MultiError {
// Log start + finish.
l := log.WithFields(kv.Fields{
{"domain", domainBlock.Domain},
{"actionID", actionID},
}...).WithContext(ctx)
l.Info("processing domain unblock side effects")
defer func() { l.Info("finished processing domain unblock side effects") }()
return p.domainUnblockSideEffects(ctx, domainBlock)
},
action,
p.state.AdminActions.DomainUnblockF(action.ID, domainBlock),
); errWithCode != nil {
return nil, actionID, errWithCode
return nil, action.ID, errWithCode
}
return apiDomainBlock, actionID, nil
}
// domainUnblockSideEffects processes the side effects of undoing a
// domain block:
//
// 1. Mark instance entry as no longer suspended.
// 2. Mark each account from the domain as no longer suspended, if the
// suspension origin corresponds to the ID of the provided domain block.
//
// It should be called asynchronously, since it can take a while when
// there are many accounts present on the given domain.
func (p *Processor) domainUnblockSideEffects(
ctx context.Context,
block *gtsmodel.DomainBlock,
) gtserror.MultiError {
var errs gtserror.MultiError
// Update instance entry for this domain, if we have it.
instance, err := p.state.DB.GetInstance(ctx, block.Domain)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
errs.Appendf("db error getting instance %s: %w", block.Domain, err)
}
if instance != nil {
// We had an entry, update it to signal
// that it's no longer suspended.
instance.SuspendedAt = time.Time{}
instance.DomainBlockID = ""
if err := p.state.DB.UpdateInstance(
ctx,
instance,
"suspended_at",
"domain_block_id",
); err != nil {
errs.Appendf("db error updating instance: %w", err)
return errs
}
}
// Unsuspend all accounts whose suspension origin was this domain block.
if err := p.rangeDomainAccounts(ctx, block.Domain, func(account *gtsmodel.Account) {
if account.SuspensionOrigin == "" || account.SuspendedAt.IsZero() {
// Account wasn't suspended, nothing to do.
return
}
if account.SuspensionOrigin != block.ID {
// Account was suspended, but not by
// this domain block, leave it alone.
return
}
// Account was suspended by this domain
// block, mark it as unsuspended.
account.SuspendedAt = time.Time{}
account.SuspensionOrigin = ""
if err := p.state.DB.UpdateAccount(
ctx,
account,
"suspended_at",
"suspension_origin",
); err != nil {
errs.Appendf("db error updating account %s: %w", account.Username, err)
}
}); err != nil {
errs.Appendf("db error ranging through accounts: %w", err)
}
return errs
return apiDomainBlock, action.ID, nil
}

View File

@@ -19,7 +19,6 @@ package admin
import (
"context"
"time"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@@ -39,47 +38,23 @@ func (p *Processor) DomainKeysExpire(
adminAcct *gtsmodel.Account,
domain string,
) (string, gtserror.WithCode) {
actionID := id.NewULID()
// Run admin action to process
// side effects of key expiry.
action := &gtsmodel.AdminAction{
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domain,
Type: gtsmodel.AdminActionExpireKeys,
AccountID: adminAcct.ID,
}
// Process key expiration asynchronously.
if errWithCode := p.actions.Run(
if errWithCode := p.state.AdminActions.Run(
ctx,
&gtsmodel.AdminAction{
ID: actionID,
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domain,
Type: gtsmodel.AdminActionExpireKeys,
AccountID: adminAcct.ID,
},
func(ctx context.Context) gtserror.MultiError {
return p.domainKeysExpireSideEffects(ctx, domain)
},
action,
p.state.AdminActions.DomainKeysExpireF(domain),
); errWithCode != nil {
return actionID, errWithCode
return action.ID, errWithCode
}
return actionID, nil
}
func (p *Processor) domainKeysExpireSideEffects(ctx context.Context, domain string) gtserror.MultiError {
var (
expiresAt = time.Now()
errs gtserror.MultiError
)
// For each account on this domain, expire
// the public key and update the account.
if err := p.rangeDomainAccounts(ctx, domain, func(account *gtsmodel.Account) {
account.PublicKeyExpiresAt = expiresAt
if err := p.state.DB.UpdateAccount(ctx,
account,
"public_key_expires_at",
); err != nil {
errs.Appendf("db error updating account: %w", err)
}
}); err != nil {
errs.Appendf("db error ranging through accounts: %w", err)
}
return errs
return action.ID, nil
}

View File

@@ -186,7 +186,7 @@ func (suite *DomainBlockTestSuite) awaitAction(actionID string) {
ctx := context.Background()
if !testrig.WaitFor(func() bool {
return suite.adminProcessor.Actions().TotalRunning() == 0
return suite.state.AdminActions.TotalRunning() == 0
}) {
suite.FailNow("timed out waiting for admin action(s) to finish")
}

View File

@@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"net/url"
"slices"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
@@ -283,3 +284,89 @@ func (p *Processor) DomainPermissionSubscriptionRemove(
return p.apiDomainPermSub(ctx, permSub)
}
func (p *Processor) DomainPermissionSubscriptionTest(
ctx context.Context,
acct *gtsmodel.Account,
id string,
) (any, 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())
}
// To process the test/dry-run correctly, we need to get
// all domain perm subs of this type with a *higher* priority,
// to know whether we ought to create permissions or not.
permSubs, err := p.state.DB.GetDomainPermissionSubscriptionsByPriority(
ctx,
permSub.PermissionType,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Find the index of the targeted
// subscription in the slice.
index := slices.IndexFunc(
permSubs,
func(ps *gtsmodel.DomainPermissionSubscription) bool {
return ps.ID == permSub.ID
},
)
// Get a transport for calling permSub.URI.
tsport, err := p.transport.NewTransportForUsername(ctx, acct.Username)
if err != nil {
err := gtserror.Newf("error getting transport: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Everything *before* the targeted
// subscription has a higher priority.
higherPrios := permSubs[:index]
// Call the permSub.URI and parse a list of perms from it.
// Any error returned here is a "real" one, not an error
// from fetching / parsing the list.
createdPerms, err := p.subscriptions.ProcessDomainPermissionSubscription(
ctx,
permSub,
tsport,
higherPrios,
true, // Dry run.
)
if err != nil {
err := gtserror.Newf("error doing dry-run: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// If permSub has an error set on it now,
// we should return it to the caller.
if permSub.Error != "" {
return map[string]string{
"error": permSub.Error,
}, nil
}
// No error, so return the list of
// perms that would have been created.
apiPerms := make([]*apimodel.DomainPermission, 0, len(createdPerms))
for _, perm := range createdPerms {
apiPerm, errWithCode := p.apiDomainPerm(ctx, perm, false)
if errWithCode != nil {
return nil, errWithCode
}
apiPerms = append(apiPerms, apiPerm)
}
return apiPerms, nil
}

View File

@@ -19,86 +19,12 @@ package admin
import (
"context"
"errors"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// stubbifyInstance renders the given instance as a stub,
// removing most information from it and marking it as
// suspended.
//
// For caller's convenience, this function returns the db
// names of all columns that are updated by it.
func stubbifyInstance(instance *gtsmodel.Instance, domainBlockID string) []string {
instance.Title = ""
instance.SuspendedAt = time.Now()
instance.DomainBlockID = domainBlockID
instance.ShortDescription = ""
instance.Description = ""
instance.Terms = ""
instance.ContactEmail = ""
instance.ContactAccountUsername = ""
instance.ContactAccountID = ""
instance.Version = ""
return []string{
"title",
"suspended_at",
"domain_block_id",
"short_description",
"description",
"terms",
"contact_email",
"contact_account_username",
"contact_account_id",
"version",
}
}
// rangeDomainAccounts iterates through all accounts
// originating from the given domain, and calls the
// provided range function on each account.
//
// If an error is returned while selecting accounts,
// the loop will stop and return the error.
func (p *Processor) rangeDomainAccounts(
ctx context.Context,
domain string,
rangeF func(*gtsmodel.Account),
) error {
var (
limit = 50 // Limit selection to avoid spiking mem/cpu.
maxID string // Start with empty string to select from top.
)
for {
// Get (next) page of accounts.
accounts, err := p.state.DB.GetInstanceAccounts(ctx, domain, maxID, limit)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// Real db error.
return gtserror.Newf("db error getting instance accounts: %w", err)
}
if len(accounts) == 0 {
// No accounts left, we're done.
return nil
}
// Set next max ID for paging down.
maxID = accounts[len(accounts)-1].ID
// Call provided range function.
for _, account := range accounts {
rangeF(account)
}
}
}
// apiDomainPerm is a cheeky shortcut for returning
// the API version of the given domain permission,
// or an appropriate error if something goes wrong.