summaryrefslogtreecommitdiff
path: root/internal/processing/account
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-01-16 17:22:44 +0100
committerLibravatar GitHub <noreply@github.com>2024-01-16 16:22:44 +0000
commitc36f9ac37b8bbdeb4def7a20ba8ea6d6b7ad12d5 (patch)
tree9569c022aaf5c4ceaaf5ce433c95d9d90b6402cc /internal/processing/account
parent[chore] Move to codeberg's exif-terminator (#2536) (diff)
downloadgotosocial-c36f9ac37b8bbdeb4def7a20ba8ea6d6b7ad12d5.tar.xz
[feature] Account alias / move API + db models (#2518)
* [feature] Account alias / move API + db models * go fmt * fix little cherry-pick issues * update error checking, formatting * add and use new util functions to simplify alias logic
Diffstat (limited to 'internal/processing/account')
-rw-r--r--internal/processing/account/alias.go149
-rw-r--r--internal/processing/account/alias_test.go161
-rw-r--r--internal/processing/account/delete.go8
-rw-r--r--internal/processing/account/delete_test.go2
-rw-r--r--internal/processing/account/move.go153
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
+}