diff options
Diffstat (limited to 'internal/processing/account')
| -rw-r--r-- | internal/processing/account/alias.go | 149 | ||||
| -rw-r--r-- | internal/processing/account/alias_test.go | 161 | ||||
| -rw-r--r-- | internal/processing/account/delete.go | 8 | ||||
| -rw-r--r-- | internal/processing/account/delete_test.go | 2 | ||||
| -rw-r--r-- | internal/processing/account/move.go | 153 | 
5 files changed, 468 insertions, 5 deletions
diff --git a/internal/processing/account/alias.go b/internal/processing/account/alias.go new file mode 100644 index 000000000..bd31e8cb2 --- /dev/null +++ b/internal/processing/account/alias.go @@ -0,0 +1,149 @@ +// 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 + +import ( +	"context" +	"errors" +	"fmt" +	"net/url" +	"slices" + +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/util" +) + +func (p *Processor) Alias( +	ctx context.Context, +	account *gtsmodel.Account, +	newAKAURIStrs []string, +) (*apimodel.Account, gtserror.WithCode) { +	if slices.Equal( +		newAKAURIStrs, +		account.AlsoKnownAsURIs, +	) { +		// No changes to do +		// here. Return early. +		return p.c.GetAPIAccountSensitive(ctx, account) +	} + +	newLen := len(newAKAURIStrs) +	if newLen == 0 { +		// Simply unset existing +		// aliases and return early. +		account.AlsoKnownAsURIs = nil +		account.AlsoKnownAs = nil + +		err := p.state.DB.UpdateAccount(ctx, account, "also_known_as_uris") +		if err != nil { +			err := gtserror.Newf("db error updating also_known_as_uri: %w", err) +			return nil, gtserror.NewErrorInternalError(err) +		} + +		return p.c.GetAPIAccountSensitive(ctx, account) +	} + +	// We need to set new AKA URIs! +	// +	// First parse them to URI ptrs and +	// normalized string representations. +	// +	// Use this cheeky type to avoid +	// repeatedly calling uri.String(). +	type uri struct { +		uri *url.URL // Parsed URI. +		str string   // uri.String(). +	} + +	newAKAs := make([]uri, newLen) +	for i, newAKAURIStr := range newAKAURIStrs { +		newAKAURI, err := url.Parse(newAKAURIStr) +		if err != nil { +			err := fmt.Errorf( +				"invalid also_known_as_uri (%s) provided in account alias request: %w", +				newAKAURIStr, err, +			) +			return nil, gtserror.NewErrorBadRequest(err, err.Error()) +		} + +		// We only deref http or https, so check this. +		if newAKAURI.Scheme != "https" && newAKAURI.Scheme != "http" { +			err := fmt.Errorf( +				"invalid also_known_as_uri (%s) provided in account alias request: %w", +				newAKAURIStr, errors.New("uri must not be empty and scheme must be http or https"), +			) +			return nil, gtserror.NewErrorBadRequest(err, err.Error()) +		} + +		newAKAs[i].uri = newAKAURI +		newAKAs[i].str = newAKAURI.String() +	} + +	// Dedupe the URI/string pairs. +	newAKAs = util.DeduplicateFunc( +		newAKAs, +		func(v uri) string { +			return v.str +		}, +	) + +	// For each deduped entry, get and +	// check the target account, and set. +	for _, newAKA := range newAKAs { +		// Don't let account do anything +		// daft by aliasing to itself. +		if newAKA.str == account.URI { +			continue +		} + +		// Ensure we have a valid, up-to-date +		// representation of the target account. +		targetAccount, _, err := p.federator.GetAccountByURI(ctx, account.Username, newAKA.uri) +		if err != nil { +			err := fmt.Errorf( +				"error dereferencing also_known_as_uri (%s) account: %w", +				newAKA.str, err, +			) +			return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error()) +		} + +		// Alias target must not be suspended. +		if !targetAccount.SuspendedAt.IsZero() { +			err := fmt.Errorf( +				"target account %s is suspended from this instance; "+ +					"you will not be able to set alsoKnownAs to that account", +				newAKA.str, +			) +			return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error()) +		} + +		// Alrighty-roo, looks good, add this one. +		account.AlsoKnownAsURIs = append(account.AlsoKnownAsURIs, newAKA.str) +		account.AlsoKnownAs = append(account.AlsoKnownAs, targetAccount) +	} + +	err := p.state.DB.UpdateAccount(ctx, account, "also_known_as_uris") +	if err != nil { +		err := gtserror.Newf("db error updating also_known_as_uri: %w", err) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	return p.c.GetAPIAccountSensitive(ctx, account) +} diff --git a/internal/processing/account/alias_test.go b/internal/processing/account/alias_test.go new file mode 100644 index 000000000..9be5721aa --- /dev/null +++ b/internal/processing/account/alias_test.go @@ -0,0 +1,161 @@ +// 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" +	"slices" +	"testing" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type AliasTestSuite struct { +	AccountStandardTestSuite +} + +func (suite *AliasTestSuite) TestAliasAccount() { +	for _, test := range []struct { +		newAliases      []string +		expectedAliases []string +		expectedErr     string +	}{ +		// Alias zork to turtle. +		{ +			newAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +			}, +			expectedAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +			}, +		}, +		// Alias zork to admin. +		{ +			newAliases: []string{ +				"http://localhost:8080/users/admin", +			}, +			expectedAliases: []string{ +				"http://localhost:8080/users/admin", +			}, +		}, +		// Alias zork to turtle AND admin. +		{ +			newAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +			}, +			expectedAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +			}, +		}, +		// Same again (noop). +		{ +			newAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +			}, +			expectedAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +			}, +		}, +		// Remove admin alias. +		{ +			newAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +			}, +			expectedAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +			}, +		}, +		// Clear aliases. +		{ +			newAliases:      []string{}, +			expectedAliases: []string{}, +		}, +		// Set bad alias. +		{ +			newAliases:  []string{"oh no"}, +			expectedErr: "invalid also_known_as_uri (oh no) provided in account alias request: uri must not be empty and scheme must be http or https", +		}, +		// Try to alias to self (won't do anything). +		{ +			newAliases: []string{ +				"http://localhost:8080/users/the_mighty_zork", +			}, +			expectedAliases: []string{}, +		}, +		// Try to alias to self and admin +		// (only non-self alias will work). +		{ +			newAliases: []string{ +				"http://localhost:8080/users/the_mighty_zork", +				"http://localhost:8080/users/admin", +			}, +			expectedAliases: []string{ +				"http://localhost:8080/users/admin", +			}, +		}, +		// Alias zork to turtle AND admin, +		// duplicates should be removed. +		{ +			newAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +				"http://localhost:8080/users/admin", +			}, +			expectedAliases: []string{ +				"http://localhost:8080/users/1happyturtle", +				"http://localhost:8080/users/admin", +			}, +		}, +	} { +		var ( +			ctx      = context.Background() +			testAcct = new(gtsmodel.Account) +		) + +		// Copy zork test account. +		*testAcct = *suite.testAccounts["local_account_1"] + +		apiAcct, err := suite.accountProcessor.Alias(ctx, testAcct, test.newAliases) +		if err != nil { +			if err.Error() != test.expectedErr { +				suite.FailNow("", "unexpected error: %s", err) +			} else { +				continue +			} +		} + +		if !slices.Equal(apiAcct.Source.AlsoKnownAsURIs, test.expectedAliases) { +			suite.FailNow("", "unexpected aliases: %+v", apiAcct.Source.AlsoKnownAsURIs) +		} +	} +} + +func TestAliasTestSuite(t *testing.T) { +	suite.Run(t, new(AliasTestSuite)) +} diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index bd320571f..ff68a4638 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -516,8 +516,8 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {  	account.Note = ""  	account.NoteRaw = ""  	account.Memorial = util.Ptr(false) -	account.AlsoKnownAs = "" -	account.MovedToAccountID = "" +	account.AlsoKnownAsURIs = nil +	account.MovedToURI = ""  	account.Reason = ""  	account.Discoverable = util.Ptr(false)  	account.StatusContentType = "" @@ -539,8 +539,8 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {  		"note",  		"note_raw",  		"memorial", -		"also_known_as", -		"moved_to_account_id", +		"also_known_as_uris", +		"moved_to_uri",  		"reason",  		"discoverable",  		"status_content_type", diff --git a/internal/processing/account/delete_test.go b/internal/processing/account/delete_test.go index 5a68eda0c..95df3cec5 100644 --- a/internal/processing/account/delete_test.go +++ b/internal/processing/account/delete_test.go @@ -65,7 +65,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {  	suite.Zero(updatedAccount.Note)  	suite.Zero(updatedAccount.NoteRaw)  	suite.False(*updatedAccount.Memorial) -	suite.Zero(updatedAccount.AlsoKnownAs) +	suite.Empty(updatedAccount.AlsoKnownAsURIs)  	suite.Zero(updatedAccount.Reason)  	suite.False(*updatedAccount.Discoverable)  	suite.Zero(updatedAccount.StatusContentType) diff --git a/internal/processing/account/move.go b/internal/processing/account/move.go new file mode 100644 index 000000000..cd5c577c6 --- /dev/null +++ b/internal/processing/account/move.go @@ -0,0 +1,153 @@ +// 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 + +import ( +	"context" +	"errors" +	"fmt" +	"net/url" +	"slices" + +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/log" +	"github.com/superseriousbusiness/gotosocial/internal/messages" +	"github.com/superseriousbusiness/gotosocial/internal/oauth" +	"golang.org/x/crypto/bcrypt" +) + +func (p *Processor) MoveSelf( +	ctx context.Context, +	authed *oauth.Auth, +	form *apimodel.AccountMoveRequest, +) gtserror.WithCode { +	// Ensure valid MovedToURI. +	if form.MovedToURI == "" { +		err := errors.New("no moved_to_uri provided in account Move request") +		return gtserror.NewErrorBadRequest(err, err.Error()) +	} + +	movedToURI, 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" { +		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()) +	} + +	// Self account Move requires password to ensure it's for real. +	if form.Password == "" { +		err := errors.New("no password provided in account Move request") +		return gtserror.NewErrorBadRequest(err, err.Error()) +	} + +	if err := bcrypt.CompareHashAndPassword( +		[]byte(authed.User.EncryptedPassword), +		[]byte(form.Password), +	); err != nil { +		err := errors.New("invalid password provided in account Move request") +		return gtserror.NewErrorBadRequest(err, err.Error()) +	} + +	var ( +		// Current account from which +		// the move is taking place. +		account = authed.Account + +		// Target account to which +		// the move is taking place. +		targetAccount *gtsmodel.Account +	) + +	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()) +	} + +	// Ensure we have a valid, up-to-date representation of the target account. +	targetAccount, _, err = p.federator.GetAccountByURI(ctx, account.Username, movedToURI) +	if err != nil { +		err := fmt.Errorf("error dereferencing moved_to_uri account: %w", err) +		return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +	} + +	if !targetAccount.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, +		) +		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) { +		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, +		) +		return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +	} + +	// Target account cannot itself have +	// already Moved somewhere else. +	if targetAccount.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, +		) +		return gtserror.NewErrorUnprocessableEntity(err, err.Error()) +	} + +	// Everything seems OK, so process the Move. +	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ +		APObjectType:   ap.ActorPerson, +		APActivityType: ap.ActivityMove, +		OriginAccount:  account, +		TargetAccount:  targetAccount, +	}) + +	return nil +}  | 
