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 +} |