diff options
Diffstat (limited to 'internal/processing')
| -rw-r--r-- | internal/processing/account/move.go | 241 | ||||
| -rw-r--r-- | internal/processing/account/move_test.go | 175 | ||||
| -rw-r--r-- | internal/processing/workers/federate.go | 66 | ||||
| -rw-r--r-- | internal/processing/workers/fromclientapi.go | 53 | ||||
| -rw-r--r-- | internal/processing/workers/fromfediapi.go | 12 | ||||
| -rw-r--r-- | internal/processing/workers/fromfediapi_move.go | 95 | ||||
| -rw-r--r-- | internal/processing/workers/util.go | 240 | ||||
| -rw-r--r-- | internal/processing/workers/wipestatus.go | 135 | ||||
| -rw-r--r-- | internal/processing/workers/workers.go | 36 | 
9 files changed, 755 insertions, 298 deletions
| diff --git a/internal/processing/account/move.go b/internal/processing/account/move.go index cd5c577c6..ca8dd4dea 100644 --- a/internal/processing/account/move.go +++ b/internal/processing/account/move.go @@ -23,14 +23,17 @@ import (  	"fmt"  	"net/url"  	"slices" +	"time"  	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/log" +	"github.com/superseriousbusiness/gotosocial/internal/id"  	"github.com/superseriousbusiness/gotosocial/internal/messages"  	"github.com/superseriousbusiness/gotosocial/internal/oauth" +	"github.com/superseriousbusiness/gotosocial/internal/uris"  	"golang.org/x/crypto/bcrypt"  ) @@ -45,13 +48,14 @@ func (p *Processor) MoveSelf(  		return gtserror.NewErrorBadRequest(err, err.Error())  	} -	movedToURI, err := url.Parse(form.MovedToURI) +	targetAcctURIStr := form.MovedToURI +	targetAcctURI, err := url.Parse(form.MovedToURI)  	if err != nil {  		err := fmt.Errorf("invalid moved_to_uri provided in account Move request: %w", err)  		return gtserror.NewErrorBadRequest(err, err.Error())  	} -	if movedToURI.Scheme != "https" && movedToURI.Scheme != "http" { +	if targetAcctURI.Scheme != "https" && targetAcctURI.Scheme != "http" {  		err := errors.New("invalid moved_to_uri provided in account Move request: uri scheme must be http or https")  		return gtserror.NewErrorBadRequest(err, err.Error())  	} @@ -70,83 +74,244 @@ func (p *Processor) MoveSelf(  		return gtserror.NewErrorBadRequest(err, err.Error())  	} +	// We can't/won't validate Move activities +	// to domains we have blocked, so check this. +	targetDomainBlocked, err := p.state.DB.IsDomainBlocked(ctx, targetAcctURI.Host) +	if err != nil { +		err := fmt.Errorf( +			"db error checking if target domain %s blocked: %w", +			targetAcctURI.Host, err, +		) +		return gtserror.NewErrorInternalError(err) +	} + +	if targetDomainBlocked { +		err := fmt.Errorf( +			"domain of %s is blocked from this instance; "+ +				"you will not be able to Move to that account", +			targetAcctURIStr, +		) +		return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +	} +  	var (  		// Current account from which  		// the move is taking place. -		account = authed.Account +		originAcct = authed.Account  		// Target account to which  		// the move is taking place. -		targetAccount *gtsmodel.Account +		targetAcct *gtsmodel.Account + +		// AP representation of target. +		targetAcctable ap.Accountable  	) -	switch { -	case account.MovedToURI == "": -		// No problemo. - -	case account.MovedToURI == form.MovedToURI: -		// Trying to move again to the same -		// destination, perhaps to reprocess -		// side effects. This is OK. -		log.Info(ctx, -			"reprocessing Move side effects from %s to %s", -			account.URI, form.MovedToURI, -		) - -	default: -		// Account already moved, and now -		// trying to move somewhere else. -		err := fmt.Errorf( -			"account %s is already Moved to %s, cannot also Move to %s", -			account.URI, account.MovedToURI, form.MovedToURI, -		) -		return gtserror.NewErrorUnprocessableEntity(err, err.Error()) -	} +	// Next steps involve checking + setting +	// state that might get messed up if a +	// client triggers this function twice +	// in quick succession, so get a lock on +	// this account. +	lockKey := originAcct.URI +	unlock := p.state.ClientLocks.Lock(lockKey) +	defer unlock()  	// Ensure we have a valid, up-to-date representation of the target account. -	targetAccount, _, err = p.federator.GetAccountByURI(ctx, account.Username, movedToURI) +	targetAcct, targetAcctable, err = p.federator.GetAccountByURI( +		ctx, +		originAcct.Username, +		targetAcctURI, +	)  	if err != nil {  		err := fmt.Errorf("error dereferencing moved_to_uri account: %w", err)  		return gtserror.NewErrorUnprocessableEntity(err, err.Error())  	} -	if !targetAccount.SuspendedAt.IsZero() { +	if !targetAcct.SuspendedAt.IsZero() {  		err := fmt.Errorf(  			"target account %s is suspended from this instance; "+  				"you will not be able to Move to that account", -			targetAccount.URI, +			targetAcct.URI,  		)  		return gtserror.NewErrorUnprocessableEntity(err, err.Error())  	} +	if targetAcct.IsRemote() { +		// Force refresh Move target account +		// to ensure we have up-to-date version. +		targetAcct, _, err = p.federator.RefreshAccount(ctx, +			originAcct.Username, +			targetAcct, +			targetAcctable, +			dereferencing.Freshest, +		) +		if err != nil { +			err := fmt.Errorf( +				"error refreshing target account %s: %w", +				targetAcctURIStr, err, +			) +			return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +		} +	} +  	// Target account MUST be aliased to this  	// account for this to be a valid Move. -	if !slices.Contains(targetAccount.AlsoKnownAsURIs, account.URI) { +	if !slices.Contains(targetAcct.AlsoKnownAsURIs, originAcct.URI) {  		err := fmt.Errorf(  			"target account %s is not aliased to this account via alsoKnownAs; "+ -				"if you just changed it, wait five minutes and try the Move again", -			targetAccount.URI, +				"if you just changed it, please wait a few minutes and try the Move again", +			targetAcct.URI,  		)  		return gtserror.NewErrorUnprocessableEntity(err, err.Error())  	}  	// Target account cannot itself have  	// already Moved somewhere else. -	if targetAccount.MovedToURI != "" { +	if targetAcct.MovedToURI != "" {  		err := fmt.Errorf(  			"target account %s has already Moved somewhere else (%s); "+  				"you will not be able to Move to that account", -			targetAccount.URI, targetAccount.MovedToURI, +			targetAcct.URI, targetAcct.MovedToURI,  		)  		return gtserror.NewErrorUnprocessableEntity(err, err.Error())  	} -	// Everything seems OK, so process the Move. +	// If a Move has been *attempted* within last 5m, +	// that involved the origin and target in any way, +	// then we shouldn't try to reprocess immediately. +	latestMoveAttempt, err := p.state.DB.GetLatestMoveAttemptInvolvingURIs( +		ctx, originAcct.URI, targetAcct.URI, +	) +	if err != nil { +		err := fmt.Errorf( +			"error checking latest Move attempt involving origin %s and target %s: %w", +			originAcct.URI, targetAcct.URI, err, +		) +		return gtserror.NewErrorInternalError(err) +	} + +	if !latestMoveAttempt.IsZero() && +		time.Since(latestMoveAttempt) < 5*time.Minute { +		err := fmt.Errorf( +			"your account or target account have been involved in a Move attempt within "+ +				"the last 5 minutes, will not process Move; please try again after %s", +			latestMoveAttempt.Add(5*time.Minute), +		) +		return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +	} + +	// If a Move has *succeeded* within the last week +	// that involved the origin and target in any way, +	// then we shouldn't process again for a while. +	latestMoveSuccess, err := p.state.DB.GetLatestMoveSuccessInvolvingURIs( +		ctx, originAcct.URI, targetAcct.URI, +	) +	if err != nil { +		err := fmt.Errorf( +			"error checking latest Move success involving origin %s and target %s: %w", +			originAcct.URI, targetAcct.URI, err, +		) +		return gtserror.NewErrorInternalError(err) +	} + +	if !latestMoveSuccess.IsZero() && +		time.Since(latestMoveSuccess) < 168*time.Hour { +		err := fmt.Errorf( +			"your account or target account have been involved in a successful Move within "+ +				"the last 7 days, will not process Move; please try again after %s", +			latestMoveSuccess.Add(168*time.Hour), +		) +		return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +	} + +	// See if we have a Move stored already +	// or if we need to create a new one. +	var move *gtsmodel.Move + +	if originAcct.MoveID != "" { +		// Move already stored, ensure it's +		// to the target and nothing weird is +		// happening with race conditions etc. +		move = originAcct.Move +		if move == nil { +			// This shouldn't happen... +			err := fmt.Errorf("nil move for id %s", originAcct.MoveID) +			return gtserror.NewErrorInternalError(err) +		} + +		if move.OriginURI != originAcct.URI || +			move.TargetURI != targetAcct.URI { +			// This is also weird... +			err := errors.New("a Move is already stored for your account but contains invalid fields") +			return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +		} + +		if originAcct.MovedToURI != move.TargetURI { +			// Huh... I'll be damned. +			err := errors.New("stored Move target URI does not equal your moved_to_uri value") +			return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +		} +	} else { +		// Move not stored yet, create it. +		moveID := id.NewULID() +		moveURIStr := uris.GenerateURIForMove(originAcct.Username, moveID) + +		// We might have selected the target +		// using the URL and not the URI. +		// Ensure we continue with the URI! +		if targetAcctURIStr != targetAcct.URI { +			targetAcctURIStr = targetAcct.URI +			targetAcctURI, err = url.Parse(targetAcctURIStr) +			if err != nil { +				return gtserror.NewErrorInternalError(err) +			} +		} + +		// Parse origin URI. +		originAcctURI, err := url.Parse(originAcct.URI) +		if err != nil { +			return gtserror.NewErrorInternalError(err) +		} + +		// Store the Move. +		move = >smodel.Move{ +			ID:          moveID, +			AttemptedAt: time.Now(), +			OriginURI:   originAcct.URI, +			Origin:      originAcctURI, +			TargetURI:   targetAcctURIStr, +			Target:      targetAcctURI, +			URI:         moveURIStr, +		} +		if err := p.state.DB.PutMove(ctx, move); err != nil { +			err := fmt.Errorf("db error storing move %s: %w", moveURIStr, err) +			return gtserror.NewErrorInternalError(err) +		} + +		// Update account with the new +		// Move, and set moved_to_uri. +		originAcct.MoveID = move.ID +		originAcct.Move = move +		originAcct.MovedToURI = targetAcct.URI +		originAcct.MovedTo = targetAcct +		if err := p.state.DB.UpdateAccount( +			ctx, +			originAcct, +			"move_id", +			"moved_to_uri", +		); err != nil { +			err := fmt.Errorf("db error updating account: %w", err) +			return gtserror.NewErrorInternalError(err) +		} +	} + +	// Everything seems OK, process Move side effects async.  	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{  		APObjectType:   ap.ActorPerson,  		APActivityType: ap.ActivityMove, -		OriginAccount:  account, -		TargetAccount:  targetAccount, +		GTSModel:       move, +		OriginAccount:  originAcct, +		TargetAccount:  targetAcct,  	})  	return nil diff --git a/internal/processing/account/move_test.go b/internal/processing/account/move_test.go new file mode 100644 index 000000000..dfa0ea4e4 --- /dev/null +++ b/internal/processing/account/move_test.go @@ -0,0 +1,175 @@ +// 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 account_test + +import ( +	"context" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +type MoveTestSuite struct { +	AccountStandardTestSuite +} + +func (suite *MoveTestSuite) TestMoveAccountOK() { +	ctx := context.Background() + +	// Copy zork. +	requestingAcct := new(gtsmodel.Account) +	*requestingAcct = *suite.testAccounts["local_account_1"] + +	// Copy admin. +	targetAcct := new(gtsmodel.Account) +	*targetAcct = *suite.testAccounts["admin_account"] + +	// Update admin to alias back to zork. +	targetAcct.AlsoKnownAsURIs = []string{requestingAcct.URI} +	if err := suite.state.DB.UpdateAccount( +		ctx, +		targetAcct, +		"also_known_as_uris", +	); err != nil { +		suite.FailNow(err.Error()) +	} + +	// Trigger move from zork to admin. +	if err := suite.accountProcessor.MoveSelf( +		ctx, +		&oauth.Auth{ +			Token:       oauth.DBTokenToToken(suite.testTokens["local_account_1"]), +			Application: suite.testApplications["local_account_1"], +			User:        suite.testUsers["local_account_1"], +			Account:     requestingAcct, +		}, +		&apimodel.AccountMoveRequest{ +			Password:   "password", +			MovedToURI: targetAcct.URI, +		}, +	); err != nil { +		suite.FailNow(err.Error()) +	} + +	// There should be a msg heading back to fromClientAPI. +	select { +	case msg := <-suite.fromClientAPIChan: +		move, ok := msg.GTSModel.(*gtsmodel.Move) +		if !ok { +			suite.FailNow("", "could not cast %T to *gtsmodel.Move", move) +		} + +		now := time.Now() +		suite.WithinDuration(now, move.CreatedAt, 5*time.Second) +		suite.WithinDuration(now, move.UpdatedAt, 5*time.Second) +		suite.WithinDuration(now, move.AttemptedAt, 5*time.Second) +		suite.Zero(move.SucceededAt) +		suite.NotZero(move.ID) +		suite.Equal(requestingAcct.URI, move.OriginURI) +		suite.NotNil(move.Origin) +		suite.Equal(targetAcct.URI, move.TargetURI) +		suite.NotNil(move.Target) +		suite.NotZero(move.URI) + +	case <-time.After(5 * time.Second): +		suite.FailNow("time out waiting for message") +	} + +	// Move should be in the database now. +	move, err := suite.state.DB.GetMoveByOriginTarget( +		ctx, +		requestingAcct.URI, +		targetAcct.URI, +	) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.NotNil(move) + +	// Origin account should have move ID and move to URI set. +	suite.Equal(move.ID, requestingAcct.MoveID) +	suite.Equal(targetAcct.URI, requestingAcct.MovedToURI) +} + +func (suite *MoveTestSuite) TestMoveAccountNotAliased() { +	ctx := context.Background() + +	// Copy zork. +	requestingAcct := new(gtsmodel.Account) +	*requestingAcct = *suite.testAccounts["local_account_1"] + +	// Don't copy admin. +	targetAcct := suite.testAccounts["admin_account"] + +	// Trigger move from zork to admin. +	// +	// Move should fail since admin is +	// not aliased back to zork. +	err := suite.accountProcessor.MoveSelf( +		ctx, +		&oauth.Auth{ +			Token:       oauth.DBTokenToToken(suite.testTokens["local_account_1"]), +			Application: suite.testApplications["local_account_1"], +			User:        suite.testUsers["local_account_1"], +			Account:     requestingAcct, +		}, +		&apimodel.AccountMoveRequest{ +			Password:   "password", +			MovedToURI: targetAcct.URI, +		}, +	) +	suite.EqualError(err, "target account http://localhost:8080/users/admin is not aliased to this account via alsoKnownAs; if you just changed it, please wait a few minutes and try the Move again") +} + +func (suite *MoveTestSuite) TestMoveAccountBadPassword() { +	ctx := context.Background() + +	// Copy zork. +	requestingAcct := new(gtsmodel.Account) +	*requestingAcct = *suite.testAccounts["local_account_1"] + +	// Don't copy admin. +	targetAcct := suite.testAccounts["admin_account"] + +	// Trigger move from zork to admin. +	// +	// Move should fail since admin is +	// not aliased back to zork. +	err := suite.accountProcessor.MoveSelf( +		ctx, +		&oauth.Auth{ +			Token:       oauth.DBTokenToToken(suite.testTokens["local_account_1"]), +			Application: suite.testApplications["local_account_1"], +			User:        suite.testUsers["local_account_1"], +			Account:     requestingAcct, +		}, +		&apimodel.AccountMoveRequest{ +			Password:   "boobies", +			MovedToURI: targetAcct.URI, +		}, +	) +	suite.EqualError(err, "invalid password provided in account Move request") +} + +func TestMoveTestSuite(t *testing.T) { +	suite.Run(t, new(MoveTestSuite)) +} diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go index aacb8dcc8..9fdb8f662 100644 --- a/internal/processing/workers/federate.go +++ b/internal/processing/workers/federate.go @@ -23,6 +23,7 @@ import (  	"github.com/superseriousbusiness/activity/pub"  	"github.com/superseriousbusiness/activity/streams" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/federation"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -954,3 +955,68 @@ func (f *federate) Flag(ctx context.Context, report *gtsmodel.Report) error {  	return nil  } + +func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) error { +	// Do nothing if it's not our +	// account that's been moved. +	if !account.IsLocal() { +		return nil +	} + +	// Parse relevant URI(s). +	outboxIRI, err := parseURI(account.OutboxURI) +	if err != nil { +		return err +	} + +	// Actor doing the Move. +	actorIRI := account.Move.Origin + +	// Destination Actor of the Move. +	targetIRI := account.Move.Target + +	followersIRI, err := parseURI(account.FollowersURI) +	if err != nil { +		return err +	} + +	publicIRI, err := parseURI(pub.PublicActivityPubIRI) +	if err != nil { +		return err +	} + +	// Create a new move. +	move := streams.NewActivityStreamsMove() + +	// Set the Move ID. +	if err := ap.SetJSONLDIdStr(move, account.Move.URI); err != nil { +		return err +	} + +	// Set the Actor for the Move. +	ap.AppendActorIRIs(move, actorIRI) + +	// Set the account's IRI as the 'object' property. +	ap.AppendObjectIRIs(move, actorIRI) + +	// Set the target's IRI as the 'target' property. +	ap.AppendTargetIRIs(move, targetIRI) + +	// Address the move To followers. +	ap.AppendTo(move, followersIRI) + +	// Address the move CC public. +	ap.AppendCc(move, publicIRI) + +	// Send the Move via the Actor's outbox. +	if _, err := f.FederatingActor().Send( +		ctx, outboxIRI, move, +	); err != nil { +		return gtserror.Newf( +			"error sending activity %T via outbox %s: %w", +			move, outboxIRI, err, +		) +	} + +	return nil +} diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go index 05b9acc1f..c7e78fee2 100644 --- a/internal/processing/workers/fromclientapi.go +++ b/internal/processing/workers/fromclientapi.go @@ -39,12 +39,12 @@ import (  // specifically for messages originating  // from the client/REST API.  type clientAPI struct { -	state      *state.State -	converter  *typeutils.Converter -	surface    *surface -	federate   *federate -	wipeStatus wipeStatus -	account    *account.Processor +	state     *state.State +	converter *typeutils.Converter +	surface   *surface +	federate  *federate +	account   *account.Processor +	utilF     *utilF  }  func (p *Processor) EnqueueClientAPI(cctx context.Context, msgs ...messages.FromClientAPI) { @@ -194,6 +194,15 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From  		case ap.ObjectProfile:  			return p.clientAPI.ReportAccount(ctx, cMsg)  		} + +	// MOVE SOMETHING +	case ap.ActivityMove: +		switch cMsg.APObjectType { //nolint:gocritic + +		// MOVE PROFILE/ACCOUNT +		case ap.ObjectProfile, ap.ActorPerson: +			return p.clientAPI.MoveAccount(ctx, cMsg) +		}  	}  	return gtserror.Newf("unhandled: %s %s", cMsg.APActivityType, cMsg.APObjectType) @@ -576,7 +585,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAP  		return gtserror.Newf("db error populating status: %w", err)  	} -	if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil { +	if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {  		log.Errorf(ctx, "error wiping status: %v", err)  	} @@ -641,3 +650,33 @@ func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientA  	return nil  } + +func (p *clientAPI) MoveAccount(ctx context.Context, cMsg messages.FromClientAPI) error { +	// Redirect each local follower of +	// OriginAccount to follow move target. +	p.utilF.redirectFollowers(ctx, cMsg.OriginAccount, cMsg.TargetAccount) + +	// At this point, we know OriginAccount has the +	// Move set on it. Just make sure it's populated. +	if err := p.state.DB.PopulateMove(ctx, cMsg.OriginAccount.Move); err != nil { +		return gtserror.Newf("error populating Move: %w", err) +	} + +	// Now send the Move message out to +	// OriginAccount's (remote) followers. +	if err := p.federate.MoveAccount(ctx, cMsg.OriginAccount); err != nil { +		return gtserror.Newf("error federating account move: %w", err) +	} + +	// Mark the move attempt as successful. +	cMsg.OriginAccount.Move.SucceededAt = cMsg.OriginAccount.Move.AttemptedAt +	if err := p.state.DB.UpdateMove( +		ctx, +		cMsg.OriginAccount.Move, +		"succeeded_at", +	); err != nil { +		return gtserror.Newf("error marking move as successful: %w", err) +	} + +	return nil +} diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index 62cb58c83..2fc3b4b26 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -39,11 +39,11 @@ import (  // specifically for messages originating  // from the federation/ActivityPub API.  type fediAPI struct { -	state      *state.State -	surface    *surface -	federate   *federate -	wipeStatus wipeStatus -	account    *account.Processor +	state    *state.State +	surface  *surface +	federate *federate +	account  *account.Processor +	utilF    *utilF  }  func (p *Processor) EnqueueFediAPI(cctx context.Context, msgs ...messages.FromFediAPI) { @@ -563,7 +563,7 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) e  		return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)  	} -	if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil { +	if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {  		log.Errorf(ctx, "error wiping status: %v", err)  	} diff --git a/internal/processing/workers/fromfediapi_move.go b/internal/processing/workers/fromfediapi_move.go index 2223a21f5..0188a5d14 100644 --- a/internal/processing/workers/fromfediapi_move.go +++ b/internal/processing/workers/fromfediapi_move.go @@ -22,7 +22,6 @@ import (  	"errors"  	"time" -	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"  	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" @@ -380,7 +379,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er  	// Transfer originAcct's followers  	// on this instance to targetAcct. -	redirectOK := p.RedirectAccountFollowers( +	redirectOK := p.utilF.redirectFollowers(  		ctx,  		originAcct,  		targetAcct, @@ -422,98 +421,6 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er  	return nil  } -// RedirectAccountFollowers redirects all local -// followers of originAcct to targetAcct. -// -// Both accounts must be fully dereferenced -// already, and the Move must be valid. -// -// Callers to this function MUST have obtained -// a lock already by calling FedLocks.Lock. -// -// Return bool will be true if all goes OK. -func (p *fediAPI) RedirectAccountFollowers( -	ctx context.Context, -	originAcct *gtsmodel.Account, -	targetAcct *gtsmodel.Account, -) bool { -	// Any local followers of originAcct should -	// send follow requests to targetAcct instead, -	// and have followers of originAcct removed. -	// -	// Select local followers with barebones, since -	// we only need follow.Account and we can get -	// that ourselves. -	followers, err := p.state.DB.GetAccountLocalFollowers( -		gtscontext.SetBarebones(ctx), -		originAcct.ID, -	) -	if err != nil && !errors.Is(err, db.ErrNoEntries) { -		log.Errorf(ctx, -			"db error getting follows targeting originAcct: %v", -			err, -		) -		return false -	} - -	for _, follow := range followers { -		// Fetch the local account that -		// owns the follow targeting originAcct. -		if follow.Account, err = p.state.DB.GetAccountByID( -			gtscontext.SetBarebones(ctx), -			follow.AccountID, -		); err != nil { -			log.Errorf(ctx, -				"db error getting follow account %s: %v", -				follow.AccountID, err, -			) -			return false -		} - -		// Use the account processor FollowCreate -		// function to send off the new follow, -		// carrying over the Reblogs and Notify -		// values from the old follow to the new. -		// -		// This will also handle cases where our -		// account has already followed the target -		// account, by just updating the existing -		// follow of target account. -		if _, err := p.account.FollowCreate( -			ctx, -			follow.Account, -			&apimodel.AccountFollowRequest{ -				ID:      targetAcct.ID, -				Reblogs: follow.ShowReblogs, -				Notify:  follow.Notify, -			}, -		); err != nil { -			log.Errorf(ctx, -				"error creating new follow for account %s: %v", -				follow.AccountID, err, -			) -			return false -		} - -		// New follow is in the process of -		// sending, remove the existing follow. -		// This will send out an Undo Activity for each Follow. -		if _, err := p.account.FollowRemove( -			ctx, -			follow.Account, -			follow.TargetAccountID, -		); err != nil { -			log.Errorf(ctx, -				"error removing old follow for account %s: %v", -				follow.AccountID, err, -			) -			return false -		} -	} - -	return true -} -  // RemoveAccountFollowing removes all  // follows owned by the move originAcct.  // diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go new file mode 100644 index 000000000..a38ecd336 --- /dev/null +++ b/internal/processing/workers/util.go @@ -0,0 +1,240 @@ +// 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 workers + +import ( +	"context" +	"errors" + +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"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/processing/account" +	"github.com/superseriousbusiness/gotosocial/internal/processing/media" +	"github.com/superseriousbusiness/gotosocial/internal/state" +) + +// utilF wraps util functions used by both +// the fromClientAPI and fromFediAPI functions. +type utilF struct { +	state   *state.State +	media   *media.Processor +	account *account.Processor +	surface *surface +} + +// wipeStatus encapsulates common logic +// used to totally delete a status + all +// its attachments, notifications, boosts, +// and timeline entries. +func (u *utilF) wipeStatus( +	ctx context.Context, +	statusToDelete *gtsmodel.Status, +	deleteAttachments bool, +) error { +	var errs gtserror.MultiError + +	// Either delete all attachments for this status, +	// or simply unattach + clean them separately later. +	// +	// Reason to unattach rather than delete is that +	// the poster might want to reattach them to another +	// status immediately (in case of delete + redraft) +	if deleteAttachments { +		// todo:u.state.DB.DeleteAttachmentsForStatus +		for _, id := range statusToDelete.AttachmentIDs { +			if err := u.media.Delete(ctx, id); err != nil { +				errs.Appendf("error deleting media: %w", err) +			} +		} +	} else { +		// todo:u.state.DB.UnattachAttachmentsForStatus +		for _, id := range statusToDelete.AttachmentIDs { +			if _, err := u.media.Unattach(ctx, statusToDelete.Account, id); err != nil { +				errs.Appendf("error unattaching media: %w", err) +			} +		} +	} + +	// delete all mention entries generated by this status +	// todo:u.state.DB.DeleteMentionsForStatus +	for _, id := range statusToDelete.MentionIDs { +		if err := u.state.DB.DeleteMentionByID(ctx, id); err != nil { +			errs.Appendf("error deleting status mention: %w", err) +		} +	} + +	// delete all notification entries generated by this status +	if err := u.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { +		errs.Appendf("error deleting status notifications: %w", err) +	} + +	// delete all bookmarks that point to this status +	if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { +		errs.Appendf("error deleting status bookmarks: %w", err) +	} + +	// delete all faves of this status +	if err := u.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { +		errs.Appendf("error deleting status faves: %w", err) +	} + +	if pollID := statusToDelete.PollID; pollID != "" { +		// Delete this poll by ID from the database. +		if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil { +			errs.Appendf("error deleting status poll: %w", err) +		} + +		// Delete any poll votes pointing to this poll ID. +		if err := u.state.DB.DeletePollVotes(ctx, pollID); err != nil { +			errs.Appendf("error deleting status poll votes: %w", err) +		} + +		// Cancel any scheduled expiry task for poll. +		_ = u.state.Workers.Scheduler.Cancel(pollID) +	} + +	// delete all boosts for this status + remove them from timelines +	boosts, err := u.state.DB.GetStatusBoosts( +		// we MUST set a barebones context here, +		// as depending on where it came from the +		// original BoostOf may already be gone. +		gtscontext.SetBarebones(ctx), +		statusToDelete.ID) +	if err != nil { +		errs.Appendf("error fetching status boosts: %w", err) +	} + +	for _, boost := range boosts { +		if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil { +			errs.Appendf("error deleting boost from timelines: %w", err) +		} +		if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil { +			errs.Appendf("error deleting boost: %w", err) +		} +	} + +	// delete this status from any and all timelines +	if err := u.surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil { +		errs.Appendf("error deleting status from timelines: %w", err) +	} + +	// finally, delete the status itself +	if err := u.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { +		errs.Appendf("error deleting status: %w", err) +	} + +	return errs.Combine() +} + +// redirectFollowers redirects all local +// followers of originAcct to targetAcct. +// +// Both accounts must be fully dereferenced +// already, and the Move must be valid. +// +// Return bool will be true if all goes OK. +func (u *utilF) redirectFollowers( +	ctx context.Context, +	originAcct *gtsmodel.Account, +	targetAcct *gtsmodel.Account, +) bool { +	// Any local followers of originAcct should +	// send follow requests to targetAcct instead, +	// and have followers of originAcct removed. +	// +	// Select local followers with barebones, since +	// we only need follow.Account and we can get +	// that ourselves. +	followers, err := u.state.DB.GetAccountLocalFollowers( +		gtscontext.SetBarebones(ctx), +		originAcct.ID, +	) +	if err != nil && !errors.Is(err, db.ErrNoEntries) { +		log.Errorf(ctx, +			"db error getting follows targeting originAcct: %v", +			err, +		) +		return false +	} + +	for _, follow := range followers { +		// Fetch the local account that +		// owns the follow targeting originAcct. +		if follow.Account, err = u.state.DB.GetAccountByID( +			gtscontext.SetBarebones(ctx), +			follow.AccountID, +		); err != nil { +			log.Errorf(ctx, +				"db error getting follow account %s: %v", +				follow.AccountID, err, +			) +			return false +		} + +		// Use the account processor FollowCreate +		// function to send off the new follow, +		// carrying over the Reblogs and Notify +		// values from the old follow to the new. +		// +		// This will also handle cases where our +		// account has already followed the target +		// account, by just updating the existing +		// follow of target account. +		// +		// Also, ensure new follow wouldn't be a +		// self follow, since that will error. +		if follow.AccountID != targetAcct.ID { +			if _, err := u.account.FollowCreate( +				ctx, +				follow.Account, +				&apimodel.AccountFollowRequest{ +					ID:      targetAcct.ID, +					Reblogs: follow.ShowReblogs, +					Notify:  follow.Notify, +				}, +			); err != nil { +				log.Errorf(ctx, +					"error creating new follow for account %s: %v", +					follow.AccountID, err, +				) +				return false +			} +		} + +		// New follow is in the process of +		// sending, remove the existing follow. +		// This will send out an Undo Activity for each Follow. +		if _, err := u.account.FollowRemove( +			ctx, +			follow.Account, +			follow.TargetAccountID, +		); err != nil { +			log.Errorf(ctx, +				"error removing old follow for account %s: %v", +				follow.AccountID, err, +			) +			return false +		} +	} + +	return true +} diff --git a/internal/processing/workers/wipestatus.go b/internal/processing/workers/wipestatus.go deleted file mode 100644 index 90a037928..000000000 --- a/internal/processing/workers/wipestatus.go +++ /dev/null @@ -1,135 +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 workers - -import ( -	"context" - -	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" -	"github.com/superseriousbusiness/gotosocial/internal/gtserror" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/processing/media" -	"github.com/superseriousbusiness/gotosocial/internal/state" -) - -// wipeStatus encapsulates common logic used to totally delete a status -// + all its attachments, notifications, boosts, and timeline entries. -type wipeStatus func(context.Context, *gtsmodel.Status, bool) error - -// wipeStatusF returns a wipeStatus util function. -func wipeStatusF(state *state.State, media *media.Processor, surface *surface) wipeStatus { -	return func( -		ctx context.Context, -		statusToDelete *gtsmodel.Status, -		deleteAttachments bool, -	) error { -		var errs gtserror.MultiError - -		// Either delete all attachments for this status, -		// or simply unattach + clean them separately later. -		// -		// Reason to unattach rather than delete is that -		// the poster might want to reattach them to another -		// status immediately (in case of delete + redraft) -		if deleteAttachments { -			// todo:state.DB.DeleteAttachmentsForStatus -			for _, id := range statusToDelete.AttachmentIDs { -				if err := media.Delete(ctx, id); err != nil { -					errs.Appendf("error deleting media: %w", err) -				} -			} -		} else { -			// todo:state.DB.UnattachAttachmentsForStatus -			for _, id := range statusToDelete.AttachmentIDs { -				if _, err := media.Unattach(ctx, statusToDelete.Account, id); err != nil { -					errs.Appendf("error unattaching media: %w", err) -				} -			} -		} - -		// delete all mention entries generated by this status -		// todo:state.DB.DeleteMentionsForStatus -		for _, id := range statusToDelete.MentionIDs { -			if err := state.DB.DeleteMentionByID(ctx, id); err != nil { -				errs.Appendf("error deleting status mention: %w", err) -			} -		} - -		// delete all notification entries generated by this status -		if err := state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { -			errs.Appendf("error deleting status notifications: %w", err) -		} - -		// delete all bookmarks that point to this status -		if err := state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { -			errs.Appendf("error deleting status bookmarks: %w", err) -		} - -		// delete all faves of this status -		if err := state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { -			errs.Appendf("error deleting status faves: %w", err) -		} - -		if pollID := statusToDelete.PollID; pollID != "" { -			// Delete this poll by ID from the database. -			if err := state.DB.DeletePollByID(ctx, pollID); err != nil { -				errs.Appendf("error deleting status poll: %w", err) -			} - -			// Delete any poll votes pointing to this poll ID. -			if err := state.DB.DeletePollVotes(ctx, pollID); err != nil { -				errs.Appendf("error deleting status poll votes: %w", err) -			} - -			// Cancel any scheduled expiry task for poll. -			_ = state.Workers.Scheduler.Cancel(pollID) -		} - -		// delete all boosts for this status + remove them from timelines -		boosts, err := state.DB.GetStatusBoosts( -			// we MUST set a barebones context here, -			// as depending on where it came from the -			// original BoostOf may already be gone. -			gtscontext.SetBarebones(ctx), -			statusToDelete.ID) -		if err != nil { -			errs.Appendf("error fetching status boosts: %w", err) -		} - -		for _, boost := range boosts { -			if err := surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil { -				errs.Appendf("error deleting boost from timelines: %w", err) -			} -			if err := state.DB.DeleteStatusByID(ctx, boost.ID); err != nil { -				errs.Appendf("error deleting boost: %w", err) -			} -		} - -		// delete this status from any and all timelines -		if err := surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil { -			errs.Appendf("error deleting status from timelines: %w", err) -		} - -		// finally, delete the status itself -		if err := state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { -			errs.Appendf("error deleting status: %w", err) -		} - -		return errs.Combine() -	} -} diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go index c0612de27..8488e501c 100644 --- a/internal/processing/workers/workers.go +++ b/internal/processing/workers/workers.go @@ -63,30 +63,30 @@ func New(  		converter: converter,  	} -	// Init shared logic wipe -	// status util func. -	wipeStatus := wipeStatusF( -		state, -		media, -		surface, -	) +	// Init shared util funcs. +	utilF := &utilF{ +		state:   state, +		media:   media, +		account: account, +		surface: surface, +	}  	return Processor{  		workers: &state.Workers,  		clientAPI: &clientAPI{ -			state:      state, -			converter:  converter, -			surface:    surface, -			federate:   federate, -			wipeStatus: wipeStatus, -			account:    account, +			state:     state, +			converter: converter, +			surface:   surface, +			federate:  federate, +			account:   account, +			utilF:     utilF,  		},  		fediAPI: &fediAPI{ -			state:      state, -			surface:    surface, -			federate:   federate, -			wipeStatus: wipeStatus, -			account:    account, +			state:    state, +			surface:  surface, +			federate: federate, +			account:  account, +			utilF:    utilF,  		},  	}  } | 
