summaryrefslogtreecommitdiff
path: root/internal/processing/account/move.go
blob: cd5c577c6029bf15f3a3eedc1e15749843b4e282 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
}