diff options
Diffstat (limited to 'internal/admin')
-rw-r--r-- | internal/admin/actions.go | 190 | ||||
-rw-r--r-- | internal/admin/actions_test.go | 180 | ||||
-rw-r--r-- | internal/admin/domainkeys.go | 51 | ||||
-rw-r--r-- | internal/admin/domainperms.go | 387 | ||||
-rw-r--r-- | internal/admin/util.go | 99 |
5 files changed, 907 insertions, 0 deletions
diff --git a/internal/admin/actions.go b/internal/admin/actions.go new file mode 100644 index 000000000..057bfe07d --- /dev/null +++ b/internal/admin/actions.go @@ -0,0 +1,190 @@ +// 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/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/workers" +) + +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 { + // Map of running actions. + running map[string]*gtsmodel.AdminAction + + // Lock for running admin actions. + // + // Not embedded struct, to shield + // from access by outside packages. + m sync.Mutex + + // DB for storing, updating, + // deleting admin actions etc. + db db.DB + + // Workers for queuing + // admin action side effects. + workers *workers.Workers +} + +func New(db db.DB, workers *workers.Workers) *Actions { + return &Actions{ + running: make(map[string]*gtsmodel.AdminAction), + db: db, + workers: workers, + } +} + +type ActionF func(context.Context) gtserror.MultiError + +// 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, + adminAction *gtsmodel.AdminAction, + f ActionF, +) gtserror.WithCode { + actionKey := adminAction.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.running[actionKey] + if ok { + a.m.Unlock() + return errActionConflict(running) + } + + // Action with this key not + // yet running, create it. + if err := a.db.PutAdminAction(ctx, adminAction); 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.running[actionKey] = adminAction + + // 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 { + adminAction.Errors = make([]string, 0, len(errs)) + for _, err := range errs { + adminAction.Errors = append(adminAction.Errors, err.Error()) + } + } + + // Action is no longer running: + // remove from running map. + a.m.Lock() + delete(a.running, actionKey) + a.m.Unlock() + + // Mark as completed in the db, + // storing errors for later review. + adminAction.CompletedAt = time.Now() + if err := a.db.UpdateAdminAction(ctx, adminAction, "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.running)) + for _, action := range a.running { + 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.running) +} diff --git a/internal/admin/actions_test.go b/internal/admin/actions_test.go new file mode 100644 index 000000000..c5084d955 --- /dev/null +++ b/internal/admin/actions_test.go @@ -0,0 +1,180 @@ +// 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" +) + +const ( + rMediaPath = "../../testrig/media" + rTemplatePath = "../../web/template" +) + +type ActionsTestSuite struct { + suite.Suite +} + +func (suite *ActionsTestSuite) SetupSuite() { + testrig.InitTestConfig() + testrig.InitTestLog() +} + +func (suite *ActionsTestSuite) TestActionOverlap() { + var ( + testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath) + ctx = context.Background() + ) + defer testrig.TearDownTestStructs(testStructs) + + // Suspend account. + action1 := >smodel.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 := >smodel.AdminAction{ + ID: id.NewULID(), + TargetCategory: gtsmodel.AdminActionCategoryAccount, + TargetID: "01H90S1CXQ97J9625C5YBXZWGT", + Type: gtsmodel.AdminActionUnsuspend, + AccountID: "01H90S1ZZXP4N74H4A9RVW1MRP", + } + key2 := action2.Key() + suite.Equal("account/01H90S1CXQ97J9625C5YBXZWGT", key2) + + errWithCode := testStructs.State.AdminActions.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 = testStructs.State.AdminActions.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 testStructs.State.AdminActions.TotalRunning() == 0 + }) { + suite.FailNow("timed out waiting for admin action(s) to finish") + } + + // Try again. + errWithCode = testStructs.State.AdminActions.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 testStructs.State.AdminActions.TotalRunning() == 0 + }) { + suite.FailNow("timed out waiting for admin action(s) to finish") + } +} + +func (suite *ActionsTestSuite) TestActionWithErrors() { + var ( + testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath) + ctx = context.Background() + ) + defer testrig.TearDownTestStructs(testStructs) + + // Suspend a domain. + action := >smodel.AdminAction{ + ID: id.NewULID(), + TargetCategory: gtsmodel.AdminActionCategoryDomain, + TargetID: "example.org", + Type: gtsmodel.AdminActionSuspend, + AccountID: "01H90S1ZZXP4N74H4A9RVW1MRP", + } + + errWithCode := testStructs.State.AdminActions.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 testStructs.State.AdminActions.TotalRunning() == 0 + }) { + suite.FailNow("timed out waiting for admin action(s) to finish") + } + + // Get action from the db. + dbAction, err := testStructs.State.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)) +} diff --git a/internal/admin/domainkeys.go b/internal/admin/domainkeys.go new file mode 100644 index 000000000..ad943ce80 --- /dev/null +++ b/internal/admin/domainkeys.go @@ -0,0 +1,51 @@ +// 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" + "time" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (a *Actions) DomainKeysExpireF(domain string) ActionF { + return func(ctx context.Context) 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 := a.rangeDomainAccounts(ctx, domain, func(account *gtsmodel.Account) { + account.PublicKeyExpiresAt = expiresAt + if err := a.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 + } +} diff --git a/internal/admin/domainperms.go b/internal/admin/domainperms.go new file mode 100644 index 000000000..fa686df43 --- /dev/null +++ b/internal/admin/domainperms.go @@ -0,0 +1,387 @@ +// 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" + "time" + + "codeberg.org/gruf/go-kv" + + "github.com/superseriousbusiness/gotosocial/internal/ap" + "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/log" + "github.com/superseriousbusiness/gotosocial/internal/messages" +) + +// Returns an AdminActionF for +// domain allow side effects. +func (a *Actions) DomainAllowF( + actionID string, + domainAllow *gtsmodel.DomainAllow, +) ActionF { + return func(ctx context.Context) gtserror.MultiError { + l := log. + WithContext(ctx). + WithFields(kv.Fields{ + {"action", "allow"}, + {"actionID", actionID}, + {"domain", domainAllow.Domain}, + }...) + + // Log start + finish. + l.Info("processing side effects") + errs := a.domainAllowSideEffects(ctx, domainAllow) + l.Info("finished processing side effects") + + return errs + } +} + +func (a *Actions) 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 := a.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 a.domainUnblockSideEffects(ctx, block) +} + +// Returns an AdminActionF for +// domain unallow side effects. +func (a *Actions) DomainUnallowF( + actionID string, + domainAllow *gtsmodel.DomainAllow, +) ActionF { + return func(ctx context.Context) gtserror.MultiError { + l := log. + WithContext(ctx). + WithFields(kv.Fields{ + {"action", "unallow"}, + {"actionID", actionID}, + {"domain", domainAllow.Domain}, + }...) + + // Log start + finish. + l.Info("processing side effects") + errs := a.domainUnallowSideEffects(ctx, domainAllow) + l.Info("finished processing side effects") + + return errs + } +} + +func (a *Actions) 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 := a.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 a.domainBlockSideEffects(ctx, block) +} + +func (a *Actions) DomainBlockF( + actionID string, + domainBlock *gtsmodel.DomainBlock, +) ActionF { + return func(ctx context.Context) gtserror.MultiError { + l := log. + WithContext(ctx). + WithFields(kv.Fields{ + {"action", "block"}, + {"actionID", actionID}, + {"domain", domainBlock.Domain}, + }...) + + skip, err := a.skipBlockSideEffects(ctx, domainBlock.Domain) + if err != nil { + return err + } + + if skip != "" { + l.Infof("skipping side effects: %s", skip) + return nil + } + + l.Info("processing side effects") + errs := a.domainBlockSideEffects(ctx, domainBlock) + l.Info("finished processing side effects") + + return 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 (a *Actions) 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 := a.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 := a.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 := a.rangeDomainAccounts(ctx, block.Domain, func(account *gtsmodel.Account) { + if err := a.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 +} + +func (a *Actions) DomainUnblockF( + actionID string, + domainBlock *gtsmodel.DomainBlock, +) ActionF { + return func(ctx context.Context) gtserror.MultiError { + l := log. + WithContext(ctx). + WithFields(kv.Fields{ + {"action", "unblock"}, + {"actionID", actionID}, + {"domain", domainBlock.Domain}, + }...) + + l.Info("processing side effects") + errs := a.domainUnblockSideEffects(ctx, domainBlock) + l.Info("finished processing side effects") + + return errs + } +} + +// 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 (a *Actions) 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 := a.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 := a.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 := a.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 := a.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 +} + +// 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 (a *Actions) 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 := a.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 +} diff --git a/internal/admin/util.go b/internal/admin/util.go new file mode 100644 index 000000000..27e81fbb3 --- /dev/null +++ b/internal/admin/util.go @@ -0,0 +1,99 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package admin + +import ( + "context" + "errors" + "time" + + "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 (a *Actions) 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 := a.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) + } + } +} |