summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing')
-rw-r--r--internal/processing/admin/domainallow.go255
-rw-r--r--internal/processing/admin/domainblock.go305
-rw-r--r--internal/processing/admin/domainblock_test.go76
-rw-r--r--internal/processing/admin/domainpermission.go335
-rw-r--r--internal/processing/admin/domainpermission_test.go280
-rw-r--r--internal/processing/admin/util.go17
6 files changed, 944 insertions, 324 deletions
diff --git a/internal/processing/admin/domainallow.go b/internal/processing/admin/domainallow.go
new file mode 100644
index 000000000..bab54e308
--- /dev/null
+++ b/internal/processing/admin/domainallow.go
@@ -0,0 +1,255 @@
+// 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"
+
+ "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"
+)
+
+func (p *Processor) createDomainAllow(
+ ctx context.Context,
+ adminAcct *gtsmodel.Account,
+ domain string,
+ obfuscate bool,
+ publicComment string,
+ privateComment string,
+ subscriptionID string,
+) (*apimodel.DomainPermission, string, gtserror.WithCode) {
+ // Check if an allow already exists for this domain.
+ domainAllow, err := p.state.DB.GetDomainAllow(ctx, domain)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ // Something went wrong in the DB.
+ err = gtserror.Newf("db error getting domain allow %s: %w", domain, err)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+
+ if domainAllow == nil {
+ // No allow exists yet, create it.
+ domainAllow = &gtsmodel.DomainAllow{
+ ID: id.NewULID(),
+ Domain: domain,
+ CreatedByAccountID: adminAcct.ID,
+ PrivateComment: text.SanitizeToPlaintext(privateComment),
+ PublicComment: text.SanitizeToPlaintext(publicComment),
+ Obfuscate: &obfuscate,
+ SubscriptionID: subscriptionID,
+ }
+
+ // Insert the new allow into the database.
+ if err := p.state.DB.CreateDomainAllow(ctx, domainAllow); err != nil {
+ err = gtserror.Newf("db error putting domain allow %s: %w", domain, err)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ actionID := id.NewULID()
+
+ // Process domain allow side
+ // effects asynchronously.
+ if errWithCode := p.actions.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)
+ },
+ ); errWithCode != nil {
+ return nil, actionID, errWithCode
+ }
+
+ apiDomainAllow, errWithCode := p.apiDomainPerm(ctx, domainAllow, false)
+ if errWithCode != nil {
+ return nil, actionID, 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)
+}
+
+func (p *Processor) deleteDomainAllow(
+ ctx context.Context,
+ adminAcct *gtsmodel.Account,
+ domainAllowID string,
+) (*apimodel.DomainPermission, string, gtserror.WithCode) {
+ domainAllow, err := p.state.DB.GetDomainAllowByID(ctx, domainAllowID)
+ if err != nil {
+ if !errors.Is(err, db.ErrNoEntries) {
+ // Real error.
+ err = gtserror.Newf("db error getting domain allow: %w", err)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+
+ // There are just no entries for this ID.
+ err = fmt.Errorf("no domain allow entry exists with ID %s", domainAllowID)
+ return nil, "", gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ // Prepare the domain allow to return, *before* the deletion goes through.
+ apiDomainAllow, errWithCode := p.apiDomainPerm(ctx, domainAllow, false)
+ if errWithCode != nil {
+ return nil, "", errWithCode
+ }
+
+ // Delete the original domain allow.
+ if err := p.state.DB.DeleteDomainAllow(ctx, domainAllow.Domain); err != nil {
+ err = gtserror.Newf("db error deleting domain allow: %w", err)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+
+ actionID := id.NewULID()
+
+ // Process domain unallow side
+ // effects asynchronously.
+ if errWithCode := p.actions.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)
+ },
+ ); errWithCode != nil {
+ return nil, actionID, 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)
+}
diff --git a/internal/processing/admin/domainblock.go b/internal/processing/admin/domainblock.go
index 1262bf6b0..4161ec12f 100644
--- a/internal/processing/admin/domainblock.go
+++ b/internal/processing/admin/domainblock.go
@@ -18,14 +18,9 @@
package admin
import (
- "bytes"
"context"
- "encoding/json"
"errors"
"fmt"
- "io"
- "mime/multipart"
- "net/http"
"time"
"codeberg.org/gruf/go-kv"
@@ -40,14 +35,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/text"
)
-// DomainBlockCreate creates an instance-level block against the given domain,
-// and then processes side effects of that block (deleting accounts, media, etc).
-//
-// If a domain block already exists for the domain, side effects will be retried.
-//
-// Return values for this function are the (new) domain block, the ID of the admin
-// action resulting from this call, and/or an error if something goes wrong.
-func (p *Processor) DomainBlockCreate(
+func (p *Processor) createDomainBlock(
ctx context.Context,
adminAcct *gtsmodel.Account,
domain string,
@@ -55,7 +43,7 @@ func (p *Processor) DomainBlockCreate(
publicComment string,
privateComment string,
subscriptionID string,
-) (*apimodel.DomainBlock, string, gtserror.WithCode) {
+) (*apimodel.DomainPermission, string, gtserror.WithCode) {
// Check if a block already exists for this domain.
domainBlock, err := p.state.DB.GetDomainBlock(ctx, domain)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
@@ -98,13 +86,22 @@ func (p *Processor) DomainBlockCreate(
Text: domainBlock.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 block side effects")
+ defer func() { l.Info("finished processing domain block side effects") }()
+
return p.domainBlockSideEffects(ctx, domainBlock)
},
); errWithCode != nil {
return nil, actionID, errWithCode
}
- apiDomainBlock, errWithCode := p.apiDomainBlock(ctx, domainBlock)
+ apiDomainBlock, errWithCode := p.apiDomainPerm(ctx, domainBlock, false)
if errWithCode != nil {
return nil, actionID, errWithCode
}
@@ -112,206 +109,6 @@ func (p *Processor) DomainBlockCreate(
return apiDomainBlock, actionID, nil
}
-// DomainBlockDelete removes one domain block with the given ID,
-// and processes side effects of removing the block asynchronously.
-//
-// Return values for this function are the deleted domain block, the ID of the admin
-// action resulting from this call, and/or an error if something goes wrong.
-func (p *Processor) DomainBlockDelete(
- ctx context.Context,
- adminAcct *gtsmodel.Account,
- domainBlockID string,
-) (*apimodel.DomainBlock, string, gtserror.WithCode) {
- domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, domainBlockID)
- if err != nil {
- if !errors.Is(err, db.ErrNoEntries) {
- // Real error.
- err = gtserror.Newf("db error getting domain block: %w", err)
- return nil, "", gtserror.NewErrorInternalError(err)
- }
-
- // There are just no entries for this ID.
- err = fmt.Errorf("no domain block entry exists with ID %s", domainBlockID)
- return nil, "", gtserror.NewErrorNotFound(err, err.Error())
- }
-
- // Prepare the domain block to return, *before* the deletion goes through.
- apiDomainBlock, errWithCode := p.apiDomainBlock(ctx, domainBlock)
- if errWithCode != nil {
- return nil, "", errWithCode
- }
-
- // Copy value of the domain block.
- domainBlockC := new(gtsmodel.DomainBlock)
- *domainBlockC = *domainBlock
-
- // Delete the original domain block.
- if err := p.state.DB.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil {
- err = gtserror.Newf("db error deleting domain block: %w", err)
- return nil, "", gtserror.NewErrorInternalError(err)
- }
-
- actionID := id.NewULID()
-
- // Process domain unblock side
- // effects asynchronously.
- if errWithCode := p.actions.Run(
- ctx,
- &gtsmodel.AdminAction{
- ID: actionID,
- TargetCategory: gtsmodel.AdminActionCategoryDomain,
- TargetID: domainBlockC.Domain,
- Type: gtsmodel.AdminActionUnsuspend,
- AccountID: adminAcct.ID,
- },
- func(ctx context.Context) gtserror.MultiError {
- return p.domainUnblockSideEffects(ctx, domainBlock)
- },
- ); errWithCode != nil {
- return nil, actionID, errWithCode
- }
-
- return apiDomainBlock, actionID, nil
-}
-
-// DomainBlocksImport handles the import of multiple domain blocks,
-// by calling the DomainBlockCreate function for each domain in the
-// provided file. Will return a slice of processed domain blocks.
-//
-// In the case of total failure, a gtserror.WithCode will be returned
-// so that the caller can respond appropriately. In the case of
-// partial or total success, a MultiStatus model will be returned,
-// which contains information about success/failure count, so that
-// the caller can retry any failures as they wish.
-func (p *Processor) DomainBlocksImport(
- ctx context.Context,
- account *gtsmodel.Account,
- domainsF *multipart.FileHeader,
-) (*apimodel.MultiStatus, gtserror.WithCode) {
- // Open the provided file.
- file, err := domainsF.Open()
- if err != nil {
- err = gtserror.Newf("error opening attachment: %w", err)
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
- }
- defer file.Close()
-
- // Copy the file contents into a buffer.
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, file)
- if err != nil {
- err = gtserror.Newf("error reading attachment: %w", err)
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- // Ensure we actually read something.
- if size == 0 {
- err = gtserror.New("error reading attachment: size 0 bytes")
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- // Parse bytes as slice of domain blocks.
- domainBlocks := make([]*apimodel.DomainBlock, 0)
- if err := json.Unmarshal(buf.Bytes(), &domainBlocks); err != nil {
- err = gtserror.Newf("error parsing attachment as domain blocks: %w", err)
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- count := len(domainBlocks)
- if count == 0 {
- err = gtserror.New("error importing domain blocks: 0 entries provided")
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- // Try to process each domain block, differentiating
- // between successes and errors so that the caller can
- // try failed imports again if desired.
- multiStatusEntries := make([]apimodel.MultiStatusEntry, 0, count)
-
- for _, domainBlock := range domainBlocks {
- var (
- domain = domainBlock.Domain.Domain
- obfuscate = domainBlock.Obfuscate
- publicComment = domainBlock.PublicComment
- privateComment = domainBlock.PrivateComment
- subscriptionID = "" // No sub ID for imports.
- errWithCode gtserror.WithCode
- )
-
- domainBlock, _, errWithCode = p.DomainBlockCreate(
- ctx,
- account,
- domain,
- obfuscate,
- publicComment,
- privateComment,
- subscriptionID,
- )
-
- var entry *apimodel.MultiStatusEntry
-
- if errWithCode != nil {
- entry = &apimodel.MultiStatusEntry{
- // Use the failed domain entry as the resource value.
- Resource: domain,
- Message: errWithCode.Safe(),
- Status: errWithCode.Code(),
- }
- } else {
- entry = &apimodel.MultiStatusEntry{
- // Use successfully created API model domain block as the resource value.
- Resource: domainBlock,
- Message: http.StatusText(http.StatusOK),
- Status: http.StatusOK,
- }
- }
-
- multiStatusEntries = append(multiStatusEntries, *entry)
- }
-
- return apimodel.NewMultiStatus(multiStatusEntries), nil
-}
-
-// DomainBlocksGet returns all existing domain blocks. If export is
-// true, the format will be suitable for writing out to an export.
-func (p *Processor) DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
- domainBlocks, err := p.state.DB.GetDomainBlocks(ctx)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("db error getting domain blocks: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- apiDomainBlocks := make([]*apimodel.DomainBlock, 0, len(domainBlocks))
- for _, domainBlock := range domainBlocks {
- apiDomainBlock, errWithCode := p.apiDomainBlock(ctx, domainBlock)
- if errWithCode != nil {
- return nil, errWithCode
- }
-
- apiDomainBlocks = append(apiDomainBlocks, apiDomainBlock)
- }
-
- return apiDomainBlocks, nil
-}
-
-// DomainBlockGet returns one domain block with the given id. If export
-// is true, the format will be suitable for writing out to an export.
-func (p *Processor) DomainBlockGet(ctx context.Context, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
- domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, id)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("no domain block exists with id %s", id)
- return nil, gtserror.NewErrorNotFound(err, err.Error())
- }
-
- // Something went wrong in the DB.
- err = gtserror.Newf("db error getting domain block %s: %w", id, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return p.apiDomainBlock(ctx, domainBlock)
-}
-
// domainBlockSideEffects processes the side effects of a domain block:
//
// 1. Strip most info away from the instance entry for the domain.
@@ -323,13 +120,6 @@ func (p *Processor) domainBlockSideEffects(
ctx context.Context,
block *gtsmodel.DomainBlock,
) gtserror.MultiError {
- l := log.
- WithContext(ctx).
- WithFields(kv.Fields{
- {"domain", block.Domain},
- }...)
- l.Debug("processing domain block side effects")
-
var errs gtserror.MultiError
// If we have an instance entry for this domain,
@@ -347,7 +137,6 @@ func (p *Processor) domainBlockSideEffects(
errs.Appendf("db error updating instance: %w", err)
return errs
}
- l.Debug("instance entry updated")
}
// For each account that belongs to this domain,
@@ -372,6 +161,68 @@ func (p *Processor) domainBlockSideEffects(
return errs
}
+func (p *Processor) deleteDomainBlock(
+ ctx context.Context,
+ adminAcct *gtsmodel.Account,
+ domainBlockID string,
+) (*apimodel.DomainPermission, string, gtserror.WithCode) {
+ domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, domainBlockID)
+ if err != nil {
+ if !errors.Is(err, db.ErrNoEntries) {
+ // Real error.
+ err = gtserror.Newf("db error getting domain block: %w", err)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+
+ // There are just no entries for this ID.
+ err = fmt.Errorf("no domain block entry exists with ID %s", domainBlockID)
+ return nil, "", gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ // Prepare the domain block to return, *before* the deletion goes through.
+ apiDomainBlock, errWithCode := p.apiDomainPerm(ctx, domainBlock, false)
+ if errWithCode != nil {
+ return nil, "", errWithCode
+ }
+
+ // Delete the original domain block.
+ if err := p.state.DB.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil {
+ err = gtserror.Newf("db error deleting domain block: %w", err)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+
+ actionID := id.NewULID()
+
+ // Process domain unblock side
+ // effects asynchronously.
+ if errWithCode := p.actions.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)
+ },
+ ); errWithCode != nil {
+ return nil, actionID, errWithCode
+ }
+
+ return apiDomainBlock, actionID, nil
+}
+
// domainUnblockSideEffects processes the side effects of undoing a
// domain block:
//
@@ -385,13 +236,6 @@ func (p *Processor) domainUnblockSideEffects(
ctx context.Context,
block *gtsmodel.DomainBlock,
) gtserror.MultiError {
- l := log.
- WithContext(ctx).
- WithFields(kv.Fields{
- {"domain", block.Domain},
- }...)
- l.Debug("processing domain unblock side effects")
-
var errs gtserror.MultiError
// Update instance entry for this domain, if we have it.
@@ -414,7 +258,6 @@ func (p *Processor) domainUnblockSideEffects(
errs.Appendf("db error updating instance: %w", err)
return errs
}
- l.Debug("instance entry updated")
}
// Unsuspend all accounts whose suspension origin was this domain block.
diff --git a/internal/processing/admin/domainblock_test.go b/internal/processing/admin/domainblock_test.go
deleted file mode 100644
index 9525ce7c3..000000000
--- a/internal/processing/admin/domainblock_test.go
+++ /dev/null
@@ -1,76 +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"
- "testing"
-
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type DomainBlockTestSuite struct {
- AdminStandardTestSuite
-}
-
-func (suite *DomainBlockTestSuite) TestCreateDomainBlock() {
- var (
- ctx = context.Background()
- adminAcct = suite.testAccounts["admin_account"]
- domain = "fossbros-anonymous.io"
- obfuscate = false
- publicComment = ""
- privateComment = ""
- subscriptionID = ""
- )
-
- apiBlock, actionID, errWithCode := suite.adminProcessor.DomainBlockCreate(
- ctx,
- adminAcct,
- domain,
- obfuscate,
- publicComment,
- privateComment,
- subscriptionID,
- )
- suite.NoError(errWithCode)
- suite.NotNil(apiBlock)
- suite.NotEmpty(actionID)
-
- // 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")
- }
-
- // Ensure action marked as
- // completed in the database.
- adminAction, err := suite.db.GetAdminAction(ctx, actionID)
- if err != nil {
- suite.FailNow(err.Error())
- }
-
- suite.NotZero(adminAction.CompletedAt)
- suite.Empty(adminAction.Errors)
-}
-
-func TestDomainBlockTestSuite(t *testing.T) {
- suite.Run(t, new(DomainBlockTestSuite))
-}
diff --git a/internal/processing/admin/domainpermission.go b/internal/processing/admin/domainpermission.go
new file mode 100644
index 000000000..c759c0f11
--- /dev/null
+++ b/internal/processing/admin/domainpermission.go
@@ -0,0 +1,335 @@
+// 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"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "mime/multipart"
+ "net/http"
+
+ 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"
+)
+
+// apiDomainPerm is a cheeky shortcut for returning
+// the API version of the given domain permission
+// (*gtsmodel.DomainBlock or *gtsmodel.DomainAllow),
+// or an appropriate error if something goes wrong.
+func (p *Processor) apiDomainPerm(
+ ctx context.Context,
+ domainPermission gtsmodel.DomainPermission,
+ export bool,
+) (*apimodel.DomainPermission, gtserror.WithCode) {
+ apiDomainPerm, err := p.tc.DomainPermToAPIDomainPerm(ctx, domainPermission, export)
+ if err != nil {
+ err := gtserror.NewfAt(3, "error converting domain permission to api model: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiDomainPerm, nil
+}
+
+// DomainPermissionCreate creates an instance-level permission
+// targeting the given domain, and then processes any side
+// effects of the permission creation.
+//
+// If the same permission type already exists for the domain,
+// side effects will be retried.
+//
+// Return values for this function are the new or existing
+// domain permission, the ID of the admin action resulting
+// from this call, and/or an error if something goes wrong.
+func (p *Processor) DomainPermissionCreate(
+ ctx context.Context,
+ permissionType gtsmodel.DomainPermissionType,
+ adminAcct *gtsmodel.Account,
+ domain string,
+ obfuscate bool,
+ publicComment string,
+ privateComment string,
+ subscriptionID string,
+) (*apimodel.DomainPermission, string, gtserror.WithCode) {
+ switch permissionType {
+
+ // Explicitly block a domain.
+ case gtsmodel.DomainPermissionBlock:
+ return p.createDomainBlock(
+ ctx,
+ adminAcct,
+ domain,
+ obfuscate,
+ publicComment,
+ privateComment,
+ subscriptionID,
+ )
+
+ // Explicitly allow a domain.
+ case gtsmodel.DomainPermissionAllow:
+ return p.createDomainAllow(
+ ctx,
+ adminAcct,
+ domain,
+ obfuscate,
+ publicComment,
+ privateComment,
+ subscriptionID,
+ )
+
+ // Weeping, roaring, red-faced.
+ default:
+ err := gtserror.Newf("unrecognized permission type %d", permissionType)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+}
+
+// DomainPermissionDelete removes one domain block with the given ID,
+// and processes side effects of removing the block asynchronously.
+//
+// Return values for this function are the deleted domain block, the ID of the admin
+// action resulting from this call, and/or an error if something goes wrong.
+func (p *Processor) DomainPermissionDelete(
+ ctx context.Context,
+ permissionType gtsmodel.DomainPermissionType,
+ adminAcct *gtsmodel.Account,
+ domainBlockID string,
+) (*apimodel.DomainPermission, string, gtserror.WithCode) {
+ switch permissionType {
+
+ // Delete explicit domain block.
+ case gtsmodel.DomainPermissionBlock:
+ return p.deleteDomainBlock(
+ ctx,
+ adminAcct,
+ domainBlockID,
+ )
+
+ // Delete explicit domain allow.
+ case gtsmodel.DomainPermissionAllow:
+ return p.deleteDomainAllow(
+ ctx,
+ adminAcct,
+ domainBlockID,
+ )
+
+ // You do the hokey-cokey and you turn
+ // around, that's what it's all about.
+ default:
+ err := gtserror.Newf("unrecognized permission type %d", permissionType)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+}
+
+// DomainPermissionsImport handles the import of multiple
+// domain permissions, by calling the DomainPermissionCreate
+// function for each domain in the provided file. Will return
+// a slice of processed domain permissions.
+//
+// In the case of total failure, a gtserror.WithCode will be
+// returned so that the caller can respond appropriately. In
+// the case of partial or total success, a MultiStatus model
+// will be returned, which contains information about success
+// + failure count, so that the caller can retry any failures
+// as they wish.
+func (p *Processor) DomainPermissionsImport(
+ ctx context.Context,
+ permissionType gtsmodel.DomainPermissionType,
+ account *gtsmodel.Account,
+ domainsF *multipart.FileHeader,
+) (*apimodel.MultiStatus, gtserror.WithCode) {
+ // Ensure known permission type.
+ if permissionType != gtsmodel.DomainPermissionBlock &&
+ permissionType != gtsmodel.DomainPermissionAllow {
+ err := gtserror.Newf("unrecognized permission type %d", permissionType)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Open the provided file.
+ file, err := domainsF.Open()
+ if err != nil {
+ err = gtserror.Newf("error opening attachment: %w", err)
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+ defer file.Close()
+
+ // Parse file as slice of domain blocks.
+ domainPerms := make([]*apimodel.DomainPermission, 0)
+ if err := json.NewDecoder(file).Decode(&domainPerms); err != nil {
+ err = gtserror.Newf("error parsing attachment as domain permissions: %w", err)
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ count := len(domainPerms)
+ if count == 0 {
+ err = gtserror.New("error importing domain permissions: 0 entries provided")
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ // Try to process each domain permission, differentiating
+ // between successes and errors so that the caller can
+ // try failed imports again if desired.
+ multiStatusEntries := make([]apimodel.MultiStatusEntry, 0, count)
+
+ for _, domainPerm := range domainPerms {
+ var (
+ domain = domainPerm.Domain.Domain
+ obfuscate = domainPerm.Obfuscate
+ publicComment = domainPerm.PublicComment
+ privateComment = domainPerm.PrivateComment
+ subscriptionID = "" // No sub ID for imports.
+ errWithCode gtserror.WithCode
+ )
+
+ domainPerm, _, errWithCode = p.DomainPermissionCreate(
+ ctx,
+ permissionType,
+ account,
+ domain,
+ obfuscate,
+ publicComment,
+ privateComment,
+ subscriptionID,
+ )
+
+ var entry *apimodel.MultiStatusEntry
+
+ if errWithCode != nil {
+ entry = &apimodel.MultiStatusEntry{
+ // Use the failed domain entry as the resource value.
+ Resource: domain,
+ Message: errWithCode.Safe(),
+ Status: errWithCode.Code(),
+ }
+ } else {
+ entry = &apimodel.MultiStatusEntry{
+ // Use successfully created API model domain block as the resource value.
+ Resource: domainPerm,
+ Message: http.StatusText(http.StatusOK),
+ Status: http.StatusOK,
+ }
+ }
+
+ multiStatusEntries = append(multiStatusEntries, *entry)
+ }
+
+ return apimodel.NewMultiStatus(multiStatusEntries), nil
+}
+
+// DomainPermissionsGet returns all existing domain
+// permissions of the requested type. If export is
+// true, the format will be suitable for writing out
+// to an export.
+func (p *Processor) DomainPermissionsGet(
+ ctx context.Context,
+ permissionType gtsmodel.DomainPermissionType,
+ account *gtsmodel.Account,
+ export bool,
+) ([]*apimodel.DomainPermission, gtserror.WithCode) {
+ var (
+ domainPerms []gtsmodel.DomainPermission
+ err error
+ )
+
+ switch permissionType {
+ case gtsmodel.DomainPermissionBlock:
+ var blocks []*gtsmodel.DomainBlock
+
+ blocks, err = p.state.DB.GetDomainBlocks(ctx)
+ if err != nil {
+ break
+ }
+
+ for _, block := range blocks {
+ domainPerms = append(domainPerms, block)
+ }
+
+ case gtsmodel.DomainPermissionAllow:
+ var allows []*gtsmodel.DomainAllow
+
+ allows, err = p.state.DB.GetDomainAllows(ctx)
+ if err != nil {
+ break
+ }
+
+ for _, allow := range allows {
+ domainPerms = append(domainPerms, allow)
+ }
+
+ default:
+ err = errors.New("unrecognized permission type")
+ }
+
+ if err != nil {
+ err := gtserror.Newf("error getting %ss: %w", permissionType.String(), err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apiDomainPerms := make([]*apimodel.DomainPermission, len(domainPerms))
+ for i, domainPerm := range domainPerms {
+ apiDomainBlock, errWithCode := p.apiDomainPerm(ctx, domainPerm, export)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ apiDomainPerms[i] = apiDomainBlock
+ }
+
+ return apiDomainPerms, nil
+}
+
+// DomainPermissionGet returns one domain
+// permission with the given id and type.
+//
+// If export is true, the format will be
+// suitable for writing out to an export.
+func (p *Processor) DomainPermissionGet(
+ ctx context.Context,
+ permissionType gtsmodel.DomainPermissionType,
+ id string,
+ export bool,
+) (*apimodel.DomainPermission, gtserror.WithCode) {
+ var (
+ domainPerm gtsmodel.DomainPermission
+ err error
+ )
+
+ switch permissionType {
+ case gtsmodel.DomainPermissionBlock:
+ domainPerm, err = p.state.DB.GetDomainBlockByID(ctx, id)
+ case gtsmodel.DomainPermissionAllow:
+ domainPerm, err = p.state.DB.GetDomainAllowByID(ctx, id)
+ default:
+ err = gtserror.New("unrecognized permission type")
+ }
+
+ if err != nil {
+ if errors.Is(err, db.ErrNoEntries) {
+ err = fmt.Errorf("no domain %s exists with id %s", permissionType.String(), id)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ err = gtserror.Newf("error getting domain %s with id %s: %w", permissionType.String(), id, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.apiDomainPerm(ctx, domainPerm, export)
+}
diff --git a/internal/processing/admin/domainpermission_test.go b/internal/processing/admin/domainpermission_test.go
new file mode 100644
index 000000000..b6de226c1
--- /dev/null
+++ b/internal/processing/admin/domainpermission_test.go
@@ -0,0 +1,280 @@
+// 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"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type DomainBlockTestSuite struct {
+ AdminStandardTestSuite
+}
+
+type domainPermAction struct {
+ // 'create' or 'delete'
+ // the domain permission.
+ createOrDelete string
+
+ // Type of permission
+ // to create or delete.
+ permissionType gtsmodel.DomainPermissionType
+
+ // Domain to target
+ // with the permission.
+ domain string
+
+ // Expected result of this
+ // permission action on each
+ // account on the target domain.
+ // Eg., suite.Zero(account.SuspendedAt)
+ expected func(*gtsmodel.Account) bool
+}
+
+type domainPermTest struct {
+ // Federation mode under which to
+ // run this test. This is important
+ // because it may effect which side
+ // effects are taken, if any.
+ instanceFederationMode string
+
+ // Series of actions to run as part
+ // of this test. After each action,
+ // expected will be called. This
+ // allows testers to run multiple
+ // actions in a row and check that
+ // the results after each action are
+ // what they expected, in light of
+ // previous actions.
+ actions []domainPermAction
+}
+
+// run a domainPermTest by running each of
+// its actions in turn and checking results.
+func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) {
+ config.SetInstanceFederationMode(t.instanceFederationMode)
+
+ for _, action := range t.actions {
+ // Run the desired action.
+ var actionID string
+ switch action.createOrDelete {
+ case "create":
+ _, actionID = suite.createDomainPerm(action.permissionType, action.domain)
+ case "delete":
+ _, actionID = suite.deleteDomainPerm(action.permissionType, action.domain)
+ default:
+ panic("createOrDelete was not 'create' or 'delete'")
+ }
+
+ // Let the action finish.
+ suite.awaitAction(actionID)
+
+ // Check expected results
+ // against each account.
+ accounts, err := suite.db.GetInstanceAccounts(
+ context.Background(),
+ action.domain,
+ "", 0,
+ )
+ if err != nil {
+ suite.FailNow("", "error getting instance accounts for %s: %v", action.domain, err)
+ }
+
+ for _, account := range accounts {
+ if !action.expected(account) {
+ suite.T().FailNow()
+ }
+ }
+ }
+}
+
+// create given permissionType with default values.
+func (suite *DomainBlockTestSuite) createDomainPerm(
+ permissionType gtsmodel.DomainPermissionType,
+ domain string,
+) (*apimodel.DomainPermission, string) {
+ ctx := context.Background()
+
+ apiPerm, actionID, errWithCode := suite.adminProcessor.DomainPermissionCreate(
+ ctx,
+ permissionType,
+ suite.testAccounts["admin_account"],
+ domain,
+ false,
+ "",
+ "",
+ "",
+ )
+ suite.NoError(errWithCode)
+ suite.NotNil(apiPerm)
+ suite.NotEmpty(actionID)
+
+ return apiPerm, actionID
+}
+
+// delete given permission type.
+func (suite *DomainBlockTestSuite) deleteDomainPerm(
+ permissionType gtsmodel.DomainPermissionType,
+ domain string,
+) (*apimodel.DomainPermission, string) {
+ var (
+ ctx = context.Background()
+ domainPermission gtsmodel.DomainPermission
+ )
+
+ // To delete the permission,
+ // first get it from the db.
+ switch permissionType {
+ case gtsmodel.DomainPermissionBlock:
+ domainPermission, _ = suite.db.GetDomainBlock(ctx, domain)
+ case gtsmodel.DomainPermissionAllow:
+ domainPermission, _ = suite.db.GetDomainAllow(ctx, domain)
+ default:
+ panic("unrecognized permission type")
+ }
+
+ if domainPermission == nil {
+ suite.FailNow("domain permission was nil")
+ }
+
+ // Now use the ID to delete it.
+ apiPerm, actionID, errWithCode := suite.adminProcessor.DomainPermissionDelete(
+ ctx,
+ permissionType,
+ suite.testAccounts["admin_account"],
+ domainPermission.GetID(),
+ )
+ suite.NoError(errWithCode)
+ suite.NotNil(apiPerm)
+ suite.NotEmpty(actionID)
+
+ return apiPerm, actionID
+}
+
+// waits for given actionID to be completed.
+func (suite *DomainBlockTestSuite) awaitAction(actionID string) {
+ ctx := context.Background()
+
+ if !testrig.WaitFor(func() bool {
+ return suite.adminProcessor.Actions().TotalRunning() == 0
+ }) {
+ suite.FailNow("timed out waiting for admin action(s) to finish")
+ }
+
+ // Ensure action marked as
+ // completed in the database.
+ adminAction, err := suite.db.GetAdminAction(ctx, actionID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.NotZero(adminAction.CompletedAt)
+ suite.Empty(adminAction.Errors)
+}
+
+func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() {
+ const domain = "fossbros-anonymous.io"
+
+ suite.runDomainPermTest(domainPermTest{
+ instanceFederationMode: config.InstanceFederationModeBlocklist,
+ actions: []domainPermAction{
+ {
+ createOrDelete: "create",
+ permissionType: gtsmodel.DomainPermissionBlock,
+ domain: domain,
+ expected: func(account *gtsmodel.Account) bool {
+ // Domain was blocked, so each
+ // account should now be suspended.
+ return suite.NotZero(account.SuspendedAt)
+ },
+ },
+ {
+ createOrDelete: "delete",
+ permissionType: gtsmodel.DomainPermissionBlock,
+ domain: domain,
+ expected: func(account *gtsmodel.Account) bool {
+ // Domain was unblocked, so each
+ // account should now be unsuspended.
+ return suite.Zero(account.SuspendedAt)
+ },
+ },
+ },
+ })
+}
+
+func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() {
+ const domain = "fossbros-anonymous.io"
+
+ suite.runDomainPermTest(domainPermTest{
+ instanceFederationMode: config.InstanceFederationModeBlocklist,
+ actions: []domainPermAction{
+ {
+ createOrDelete: "create",
+ permissionType: gtsmodel.DomainPermissionBlock,
+ domain: domain,
+ expected: func(account *gtsmodel.Account) bool {
+ // Domain was blocked, so each
+ // account should now be suspended.
+ return suite.NotZero(account.SuspendedAt)
+ },
+ },
+ {
+ createOrDelete: "create",
+ permissionType: gtsmodel.DomainPermissionAllow,
+ domain: domain,
+ expected: func(account *gtsmodel.Account) bool {
+ // Domain was explicitly allowed, so each
+ // account should now be unsuspended, since
+ // the allow supercedes the block.
+ return suite.Zero(account.SuspendedAt)
+ },
+ },
+ {
+ createOrDelete: "delete",
+ permissionType: gtsmodel.DomainPermissionAllow,
+ domain: domain,
+ expected: func(account *gtsmodel.Account) bool {
+ // Deleting the allow now, while there's
+ // still a block in place, should cause
+ // the block to take effect again.
+ return suite.NotZero(account.SuspendedAt)
+ },
+ },
+ {
+ createOrDelete: "delete",
+ permissionType: gtsmodel.DomainPermissionBlock,
+ domain: domain,
+ expected: func(account *gtsmodel.Account) bool {
+ // Deleting the block now should
+ // unsuspend the accounts again.
+ return suite.Zero(account.SuspendedAt)
+ },
+ },
+ },
+ })
+}
+
+func TestDomainBlockTestSuite(t *testing.T) {
+ suite.Run(t, new(DomainBlockTestSuite))
+}
diff --git a/internal/processing/admin/util.go b/internal/processing/admin/util.go
index 403602901..c82ff2dc1 100644
--- a/internal/processing/admin/util.go
+++ b/internal/processing/admin/util.go
@@ -22,28 +22,11 @@ import (
"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"
)
-// apiDomainBlock is a cheeky shortcut for returning
-// the API version of the given domainBlock, or an
-// appropriate error if something goes wrong.
-func (p *Processor) apiDomainBlock(
- ctx context.Context,
- domainBlock *gtsmodel.DomainBlock,
-) (*apimodel.DomainBlock, gtserror.WithCode) {
- apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, false)
- if err != nil {
- err = gtserror.Newf("error converting domain block for %s to api model : %w", domainBlock.Domain, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return apiDomainBlock, nil
-}
-
// stubbifyInstance renders the given instance as a stub,
// removing most information from it and marking it as
// suspended.