mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-25 21:26:40 +00:00
[bugfix] boost and account recursion (#2982)
* fix possible infinite recursion if moved accounts are self-referential * adds a defensive check for a boost being a boost of a boost wrapper * add checks on input for a boost of a boost * remove unnecessary check * add protections on account move to prevent move recursion loops * separate status conversion without boost logic into separate function to remove risk of recursion * move boost check to boost function itself * formatting * use error 422 instead of 500 * use gtserror not standard errors package for error creation
This commit is contained in:
parent
ebdcb00d0a
commit
fd6637df4a
|
@ -57,6 +57,9 @@ type Account interface {
|
||||||
// GetAccountByFollowersURI returns one account with the given followers_uri, or an error if something goes wrong.
|
// GetAccountByFollowersURI returns one account with the given followers_uri, or an error if something goes wrong.
|
||||||
GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||||
|
|
||||||
|
// GetAccountByMovedToURI returns any accounts with given moved_to_uri set.
|
||||||
|
GetAccountsByMovedToURI(ctx context.Context, uri string) ([]*gtsmodel.Account, error)
|
||||||
|
|
||||||
// GetAccounts returns accounts
|
// GetAccounts returns accounts
|
||||||
// with the given parameters.
|
// with the given parameters.
|
||||||
GetAccounts(
|
GetAccounts(
|
||||||
|
|
|
@ -252,6 +252,27 @@ func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gts
|
||||||
return a.GetAccountByUsernameDomain(ctx, username, domain)
|
return a.GetAccountByUsernameDomain(ctx, username, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *accountDB) GetAccountsByMovedToURI(ctx context.Context, uri string) ([]*gtsmodel.Account, error) {
|
||||||
|
var accountIDs []string
|
||||||
|
|
||||||
|
// Find all account IDs with
|
||||||
|
// given moved_to_uri column.
|
||||||
|
if err := a.db.NewSelect().
|
||||||
|
Table("accounts").
|
||||||
|
Column("id").
|
||||||
|
Where("? = ?", bun.Ident("moved_to_uri"), uri).
|
||||||
|
Scan(ctx, &accountIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(accountIDs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return account models for all found IDs.
|
||||||
|
return a.GetAccountsByIDs(ctx, accountIDs)
|
||||||
|
}
|
||||||
|
|
||||||
// GetAccounts selects accounts using the given parameters.
|
// GetAccounts selects accounts using the given parameters.
|
||||||
// Unlike with other functions, the paging for GetAccounts
|
// Unlike with other functions, the paging for GetAccounts
|
||||||
// is done not by ID, but by a concatenation of `[domain]/@[username]`,
|
// is done not by ID, but by a concatenation of `[domain]/@[username]`,
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
@ -56,25 +55,17 @@ func (d *Dereferencer) EnrichAnnounce(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch/deref status being boosted.
|
// Fetch and dereference status being boosted, noting that
|
||||||
var target *gtsmodel.Status
|
// d.GetStatusByURI handles domain blocks and local statuses.
|
||||||
|
target, _, err := d.GetStatusByURI(ctx, requestUser, targetURIObj)
|
||||||
if targetURIObj.Host == config.GetHost() {
|
if err != nil {
|
||||||
// This is a local status, fetch from the database
|
return nil, gtserror.Newf("error fetching boost target %s: %w", targetURI, err)
|
||||||
target, err = d.state.DB.GetStatusByURI(ctx, targetURI)
|
|
||||||
} else {
|
|
||||||
// This is a remote status, we need to dereference it.
|
|
||||||
//
|
|
||||||
// d.GetStatusByURI will handle domain block checking for us,
|
|
||||||
// so we don't try to deref an announce target on a blocked host.
|
|
||||||
target, _, err = d.GetStatusByURI(ctx, requestUser, targetURIObj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if target.BoostOfID != "" {
|
||||||
return nil, gtserror.Newf(
|
// Ensure that the target is not a boost (should not be possible).
|
||||||
"error getting boost target status %s: %w",
|
err := gtserror.Newf("target status %s is a boost", targetURI)
|
||||||
targetURI, err,
|
return nil, err
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate an ID for the boost wrapper status.
|
// Generate an ID for the boost wrapper status.
|
||||||
|
|
|
@ -27,7 +27,9 @@
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
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/federation/dereferencing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
@ -44,8 +46,8 @@ func (p *Processor) MoveSelf(
|
||||||
) gtserror.WithCode {
|
) gtserror.WithCode {
|
||||||
// Ensure valid MovedToURI.
|
// Ensure valid MovedToURI.
|
||||||
if form.MovedToURI == "" {
|
if form.MovedToURI == "" {
|
||||||
err := errors.New("no moved_to_uri provided in account Move request")
|
const text = "no moved_to_uri provided in Move request"
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctURIStr := form.MovedToURI
|
targetAcctURIStr := form.MovedToURI
|
||||||
|
@ -56,29 +58,30 @@ func (p *Processor) MoveSelf(
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetAcctURI.Scheme != "https" && targetAcctURI.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")
|
const text = "invalid move_to_uri in Move request: scheme must be http(s)"
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Self account Move requires password to ensure it's for real.
|
// Self account Move requires
|
||||||
|
// password to ensure it's for real.
|
||||||
if form.Password == "" {
|
if form.Password == "" {
|
||||||
err := errors.New("no password provided in account Move request")
|
const text = "no password provided in Move request"
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword(
|
if err := bcrypt.CompareHashAndPassword(
|
||||||
[]byte(authed.User.EncryptedPassword),
|
[]byte(authed.User.EncryptedPassword),
|
||||||
[]byte(form.Password),
|
[]byte(form.Password),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
err := errors.New("invalid password provided in account Move request")
|
const text = "invalid password provided in Move request"
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can't/won't validate Move activities
|
// We can't/won't validate Move activities
|
||||||
// to domains we have blocked, so check this.
|
// to domains we have blocked, so check this.
|
||||||
targetDomainBlocked, err := p.state.DB.IsDomainBlocked(ctx, targetAcctURI.Host)
|
targetDomainBlocked, err := p.state.DB.IsDomainBlocked(ctx, targetAcctURI.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf(
|
err := gtserror.Newf(
|
||||||
"db error checking if target domain %s blocked: %w",
|
"db error checking if target domain %s blocked: %w",
|
||||||
targetAcctURI.Host, err,
|
targetAcctURI.Host, err,
|
||||||
)
|
)
|
||||||
|
@ -86,12 +89,12 @@ func (p *Processor) MoveSelf(
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetDomainBlocked {
|
if targetDomainBlocked {
|
||||||
err := fmt.Errorf(
|
text := fmt.Sprintf(
|
||||||
"domain of %s is blocked from this instance; "+
|
"domain of %s is blocked from this instance; "+
|
||||||
"you will not be able to Move to that account",
|
"you will not be able to Move to that account",
|
||||||
targetAcctURIStr,
|
targetAcctURIStr,
|
||||||
)
|
)
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -123,22 +126,24 @@ func (p *Processor) MoveSelf(
|
||||||
targetAcctURI,
|
targetAcctURI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("error dereferencing moved_to_uri account: %w", err)
|
const text = "error dereferencing moved_to_uri"
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
err := gtserror.Newf("error dereferencing move_to_uri: %w", err)
|
||||||
|
return gtserror.NewErrorUnprocessableEntity(err, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !targetAcct.SuspendedAt.IsZero() {
|
if !targetAcct.SuspendedAt.IsZero() {
|
||||||
err := fmt.Errorf(
|
text := fmt.Sprintf(
|
||||||
"target account %s is suspended from this instance; "+
|
"target account %s is suspended from this instance; "+
|
||||||
"you will not be able to Move to that account",
|
"you will not be able to Move to that account",
|
||||||
targetAcct.URI,
|
targetAcct.URI,
|
||||||
)
|
)
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetAcct.IsRemote() {
|
if targetAcctable == nil {
|
||||||
// Force refresh Move target account
|
// Target account was not dereferenced, now
|
||||||
// to ensure we have up-to-date version.
|
// force refresh Move target account to ensure we
|
||||||
|
// have most up-to-date version (non remote = no-op).
|
||||||
targetAcct, _, err = p.federator.RefreshAccount(ctx,
|
targetAcct, _, err = p.federator.RefreshAccount(ctx,
|
||||||
originAcct.Username,
|
originAcct.Username,
|
||||||
targetAcct,
|
targetAcct,
|
||||||
|
@ -146,11 +151,9 @@ func (p *Processor) MoveSelf(
|
||||||
dereferencing.Freshest,
|
dereferencing.Freshest,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf(
|
const text = "error dereferencing moved_to_uri"
|
||||||
"error refreshing target account %s: %w",
|
err := gtserror.Newf("error dereferencing move_to_uri: %w", err)
|
||||||
targetAcctURIStr, err,
|
return gtserror.NewErrorUnprocessableEntity(err, text)
|
||||||
)
|
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,33 +161,41 @@ func (p *Processor) MoveSelf(
|
||||||
// this move reattempt is to the same account.
|
// this move reattempt is to the same account.
|
||||||
if originAcct.IsMoving() &&
|
if originAcct.IsMoving() &&
|
||||||
originAcct.MovedToURI != targetAcct.URI {
|
originAcct.MovedToURI != targetAcct.URI {
|
||||||
err := fmt.Errorf(
|
text := fmt.Sprintf(
|
||||||
"your account is already Moving or has Moved to %s; you cannot also Move to %s",
|
"your account is already Moving or has Moved to %s; you cannot also Move to %s",
|
||||||
originAcct.MovedToURI, targetAcct.URI,
|
originAcct.MovedToURI, targetAcct.URI,
|
||||||
)
|
)
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target account MUST be aliased to this
|
// Target account MUST be aliased to this
|
||||||
// account for this to be a valid Move.
|
// account for this to be a valid Move.
|
||||||
if !slices.Contains(targetAcct.AlsoKnownAsURIs, originAcct.URI) {
|
if !slices.Contains(targetAcct.AlsoKnownAsURIs, originAcct.URI) {
|
||||||
err := fmt.Errorf(
|
text := fmt.Sprintf(
|
||||||
"target account %s is not aliased to this account via alsoKnownAs; "+
|
"target account %s is not aliased to this account via alsoKnownAs; "+
|
||||||
"if you just changed it, please wait a few minutes and try the Move again",
|
"if you just changed it, please wait a few minutes and try the Move again",
|
||||||
targetAcct.URI,
|
targetAcct.URI,
|
||||||
)
|
)
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target account cannot itself have
|
// Target account cannot itself have
|
||||||
// already Moved somewhere else.
|
// already Moved somewhere else.
|
||||||
if targetAcct.MovedToURI != "" {
|
if targetAcct.MovedToURI != "" {
|
||||||
err := fmt.Errorf(
|
text := fmt.Sprintf(
|
||||||
"target account %s has already Moved somewhere else (%s); "+
|
"target account %s has already Moved somewhere else (%s); "+
|
||||||
"you will not be able to Move to that account",
|
"you will not be able to Move to that account",
|
||||||
targetAcct.URI, targetAcct.MovedToURI,
|
targetAcct.URI, targetAcct.MovedToURI,
|
||||||
)
|
)
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check this isn't a recursive loop of moves.
|
||||||
|
if errWithCode := p.checkMoveRecursion(ctx,
|
||||||
|
originAcct,
|
||||||
|
targetAcct,
|
||||||
|
); errWithCode != nil {
|
||||||
|
return errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a Move has been *attempted* within last 5m,
|
// If a Move has been *attempted* within last 5m,
|
||||||
|
@ -194,7 +205,7 @@ func (p *Processor) MoveSelf(
|
||||||
ctx, originAcct.URI, targetAcct.URI,
|
ctx, originAcct.URI, targetAcct.URI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf(
|
err := gtserror.Newf(
|
||||||
"error checking latest Move attempt involving origin %s and target %s: %w",
|
"error checking latest Move attempt involving origin %s and target %s: %w",
|
||||||
originAcct.URI, targetAcct.URI, err,
|
originAcct.URI, targetAcct.URI, err,
|
||||||
)
|
)
|
||||||
|
@ -203,12 +214,12 @@ func (p *Processor) MoveSelf(
|
||||||
|
|
||||||
if !latestMoveAttempt.IsZero() &&
|
if !latestMoveAttempt.IsZero() &&
|
||||||
time.Since(latestMoveAttempt) < 5*time.Minute {
|
time.Since(latestMoveAttempt) < 5*time.Minute {
|
||||||
err := fmt.Errorf(
|
text := fmt.Sprintf(
|
||||||
"your account or target account have been involved in a Move attempt within "+
|
"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",
|
"the last 5 minutes, will not process Move; please try again after %s",
|
||||||
latestMoveAttempt.Add(5*time.Minute),
|
latestMoveAttempt.Add(5*time.Minute),
|
||||||
)
|
)
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a Move has *succeeded* within the last week
|
// If a Move has *succeeded* within the last week
|
||||||
|
@ -218,7 +229,7 @@ func (p *Processor) MoveSelf(
|
||||||
ctx, originAcct.URI, targetAcct.URI,
|
ctx, originAcct.URI, targetAcct.URI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf(
|
err := gtserror.Newf(
|
||||||
"error checking latest Move success involving origin %s and target %s: %w",
|
"error checking latest Move success involving origin %s and target %s: %w",
|
||||||
originAcct.URI, targetAcct.URI, err,
|
originAcct.URI, targetAcct.URI, err,
|
||||||
)
|
)
|
||||||
|
@ -227,12 +238,12 @@ func (p *Processor) MoveSelf(
|
||||||
|
|
||||||
if !latestMoveSuccess.IsZero() &&
|
if !latestMoveSuccess.IsZero() &&
|
||||||
time.Since(latestMoveSuccess) < 168*time.Hour {
|
time.Since(latestMoveSuccess) < 168*time.Hour {
|
||||||
err := fmt.Errorf(
|
text := fmt.Sprintf(
|
||||||
"your account or target account have been involved in a successful Move within "+
|
"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",
|
"the last 7 days, will not process Move; please try again after %s",
|
||||||
latestMoveSuccess.Add(168*time.Hour),
|
latestMoveSuccess.Add(168*time.Hour),
|
||||||
)
|
)
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if we have a Move stored already
|
// See if we have a Move stored already
|
||||||
|
@ -246,21 +257,21 @@ func (p *Processor) MoveSelf(
|
||||||
move = originAcct.Move
|
move = originAcct.Move
|
||||||
if move == nil {
|
if move == nil {
|
||||||
// This shouldn't happen...
|
// This shouldn't happen...
|
||||||
err := fmt.Errorf("nil move for id %s", originAcct.MoveID)
|
err := gtserror.Newf("error fetching move %s (was nil)", originAcct.MovedToURI)
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if move.OriginURI != originAcct.URI ||
|
if move.OriginURI != originAcct.URI ||
|
||||||
move.TargetURI != targetAcct.URI {
|
move.TargetURI != targetAcct.URI {
|
||||||
// This is also weird...
|
// This is also weird...
|
||||||
err := errors.New("a Move is already stored for your account but contains invalid fields")
|
const text = "existing stored Move contains invalid fields"
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if originAcct.MovedToURI != move.TargetURI {
|
if originAcct.MovedToURI != move.TargetURI {
|
||||||
// Huh... I'll be damned.
|
// Huh... I'll be damned.
|
||||||
err := errors.New("stored Move target URI does not equal your moved_to_uri value")
|
const text = "existing stored Move target URI != moved_to_uri"
|
||||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Move not stored yet, create it.
|
// Move not stored yet, create it.
|
||||||
|
@ -295,7 +306,7 @@ func (p *Processor) MoveSelf(
|
||||||
URI: moveURIStr,
|
URI: moveURIStr,
|
||||||
}
|
}
|
||||||
if err := p.state.DB.PutMove(ctx, move); err != nil {
|
if err := p.state.DB.PutMove(ctx, move); err != nil {
|
||||||
err := fmt.Errorf("db error storing move %s: %w", moveURIStr, err)
|
err := gtserror.Newf("db error storing move %s: %w", moveURIStr, err)
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +322,7 @@ func (p *Processor) MoveSelf(
|
||||||
"move_id",
|
"move_id",
|
||||||
"moved_to_uri",
|
"moved_to_uri",
|
||||||
); err != nil {
|
); err != nil {
|
||||||
err := fmt.Errorf("db error updating account: %w", err)
|
err := gtserror.Newf("db error updating account: %w", err)
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,3 +338,55 @@ func (p *Processor) MoveSelf(
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkMoveRecursion checks that a move from origin to target would
|
||||||
|
// not cause a loop of account moved_from_uris pointing in a loop.
|
||||||
|
func (p *Processor) checkMoveRecursion(
|
||||||
|
ctx context.Context,
|
||||||
|
origin *gtsmodel.Account,
|
||||||
|
target *gtsmodel.Account,
|
||||||
|
) gtserror.WithCode {
|
||||||
|
// We only ever need barebones models.
|
||||||
|
ctx = gtscontext.SetBarebones(ctx)
|
||||||
|
|
||||||
|
// Stack based account move following loop.
|
||||||
|
stack := []*gtsmodel.Account{origin}
|
||||||
|
checked := make(map[string]struct{})
|
||||||
|
for len(stack) > 0 {
|
||||||
|
|
||||||
|
// Pop account from stack.
|
||||||
|
next := stack[len(stack)-1]
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
|
||||||
|
// Add account URI to checked.
|
||||||
|
checked[next.URI] = struct{}{}
|
||||||
|
|
||||||
|
// Fetch any accounts that list 'next' as their 'moved_to_uri'.
|
||||||
|
movedFrom, err := p.state.DB.GetAccountsByMovedToURI(ctx, next.URI)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.Newf("error fetching accounts by moved_to_uri: %w", err)
|
||||||
|
return gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range movedFrom {
|
||||||
|
if _, ok := checked[account.URI]; ok {
|
||||||
|
// Account with URI has
|
||||||
|
// already been checked.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check movedFrom accounts to ensure
|
||||||
|
// none of them actually come from target,
|
||||||
|
// which would cause a recursion loop.
|
||||||
|
if account.URI == target.URI {
|
||||||
|
text := fmt.Sprintf("move %s -> %s would cause move recursion due to %s", origin.URI, target.URI, account.URI)
|
||||||
|
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append 'from' account to stack.
|
||||||
|
stack = append(stack, account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@ func (suite *MoveTestSuite) TestMoveAccountBadPassword() {
|
||||||
MovedToURI: targetAcct.URI,
|
MovedToURI: targetAcct.URI,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
suite.EqualError(err, "invalid password provided in account Move request")
|
suite.EqualError(err, "invalid password provided in Move request")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoveTestSuite(t *testing.T) {
|
func TestMoveTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -49,6 +49,7 @@ func (p *Processor) BoostCreate(
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap target in case it is a boost.
|
||||||
target, errWithCode = p.c.UnwrapIfBoost(
|
target, errWithCode = p.c.UnwrapIfBoost(
|
||||||
ctx,
|
ctx,
|
||||||
requester,
|
requester,
|
||||||
|
@ -58,7 +59,13 @@ func (p *Processor) BoostCreate(
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure valid boost target.
|
// Check is viable target.
|
||||||
|
if target.BoostOfID != "" {
|
||||||
|
err := gtserror.Newf("target status %s is boost wrapper", target.URI)
|
||||||
|
return nil, gtserror.NewErrorUnprocessableEntity(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure valid boost target for requester.
|
||||||
boostable, err := p.filter.StatusBoostable(ctx,
|
boostable, err := p.filter.StatusBoostable(ctx,
|
||||||
requester,
|
requester,
|
||||||
target,
|
target,
|
||||||
|
|
|
@ -147,6 +147,24 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
|
||||||
// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields.
|
// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields.
|
||||||
// In other words, this is the public record that the server has of an account.
|
// In other words, this is the public record that the server has of an account.
|
||||||
func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
||||||
|
account, err := c.accountToAPIAccountPublic(ctx, a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.MovedTo != nil {
|
||||||
|
account.Moved, err = c.accountToAPIAccountPublic(ctx, a.MovedTo)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "error converting account movedTo: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// accountToAPIAccountPublic provides all the logic for AccountToAPIAccount, MINUS fetching moved account, to prevent possible recursion.
|
||||||
|
func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
||||||
|
|
||||||
// Populate account struct fields.
|
// Populate account struct fields.
|
||||||
err := c.state.DB.PopulateAccount(ctx, a)
|
err := c.state.DB.PopulateAccount(ctx, a)
|
||||||
|
|
||||||
|
@ -154,7 +172,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
case err == nil:
|
case err == nil:
|
||||||
// No problem.
|
// No problem.
|
||||||
|
|
||||||
case err != nil && a.Stats != nil:
|
case a.Stats != nil:
|
||||||
// We have stats so that's
|
// We have stats so that's
|
||||||
// *maybe* OK, try to continue.
|
// *maybe* OK, try to continue.
|
||||||
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
|
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
|
||||||
|
@ -266,37 +284,10 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
acct = a.Username // omit domain
|
acct = a.Username // omit domain
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate moved.
|
|
||||||
var moved *apimodel.Account
|
|
||||||
if a.MovedTo != nil {
|
|
||||||
moved, err = c.AccountToAPIAccountPublic(ctx, a.MovedTo)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf(ctx, "error converting account movedTo: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bool ptrs should be set, but warn
|
|
||||||
// and use a default if they're not.
|
|
||||||
var boolPtrDef = func(
|
|
||||||
pName string,
|
|
||||||
p *bool,
|
|
||||||
d bool,
|
|
||||||
) bool {
|
|
||||||
if p != nil {
|
|
||||||
return *p
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warnf(ctx,
|
|
||||||
"%s ptr was nil, using default %t",
|
|
||||||
pName, d,
|
|
||||||
)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
locked = boolPtrDef("locked", a.Locked, true)
|
locked = util.PtrValueOr(a.Locked, true)
|
||||||
discoverable = boolPtrDef("discoverable", a.Discoverable, false)
|
discoverable = util.PtrValueOr(a.Discoverable, false)
|
||||||
bot = boolPtrDef("bot", a.Bot, false)
|
bot = util.PtrValueOr(a.Bot, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remaining properties are simple and
|
// Remaining properties are simple and
|
||||||
|
@ -329,7 +320,6 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
||||||
EnableRSS: enableRSS,
|
EnableRSS: enableRSS,
|
||||||
HideCollections: hideCollections,
|
HideCollections: hideCollections,
|
||||||
Role: role,
|
Role: role,
|
||||||
Moved: moved,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bodge default avatar + header in,
|
// Bodge default avatar + header in,
|
||||||
|
@ -350,7 +340,8 @@ func (c *Converter) fieldsToAPIFields(f []*gtsmodel.Field) []apimodel.Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !field.VerifiedAt.IsZero() {
|
if !field.VerifiedAt.IsZero() {
|
||||||
mField.VerifiedAt = func() *string { s := util.FormatISO8601(field.VerifiedAt); return &s }()
|
verified := util.FormatISO8601(field.VerifiedAt)
|
||||||
|
mField.VerifiedAt = util.Ptr(verified)
|
||||||
}
|
}
|
||||||
|
|
||||||
fields[i] = mField
|
fields[i] = mField
|
||||||
|
@ -755,6 +746,10 @@ func (c *Converter) StatusToAPIStatus(
|
||||||
var aside string
|
var aside string
|
||||||
aside, apiStatus.MediaAttachments = placeholdUnknownAttachments(apiStatus.MediaAttachments)
|
aside, apiStatus.MediaAttachments = placeholdUnknownAttachments(apiStatus.MediaAttachments)
|
||||||
apiStatus.Content += aside
|
apiStatus.Content += aside
|
||||||
|
if apiStatus.Reblog != nil {
|
||||||
|
aside, apiStatus.Reblog.MediaAttachments = placeholdUnknownAttachments(apiStatus.Reblog.MediaAttachments)
|
||||||
|
apiStatus.Reblog.Content += aside
|
||||||
|
}
|
||||||
|
|
||||||
return apiStatus, nil
|
return apiStatus, nil
|
||||||
}
|
}
|
||||||
|
@ -1050,29 +1045,83 @@ func (c *Converter) StatusToAPIStatusSource(ctx context.Context, s *gtsmodel.Sta
|
||||||
//
|
//
|
||||||
// Requesting account can be nil.
|
// Requesting account can be nil.
|
||||||
func (c *Converter) statusToFrontend(
|
func (c *Converter) statusToFrontend(
|
||||||
|
ctx context.Context,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
requestingAccount *gtsmodel.Account,
|
||||||
|
filterContext statusfilter.FilterContext,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
|
mutes *usermute.CompiledUserMuteList,
|
||||||
|
) (
|
||||||
|
*apimodel.Status,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
apiStatus, err := c.baseStatusToFrontend(ctx,
|
||||||
|
status,
|
||||||
|
requestingAccount,
|
||||||
|
filterContext,
|
||||||
|
filters,
|
||||||
|
mutes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.BoostOf != nil {
|
||||||
|
reblog, err := c.baseStatusToFrontend(ctx,
|
||||||
|
status.BoostOf,
|
||||||
|
requestingAccount,
|
||||||
|
filterContext,
|
||||||
|
filters,
|
||||||
|
mutes,
|
||||||
|
)
|
||||||
|
if errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
// If we'd hide the original status, hide the boost.
|
||||||
|
return nil, err
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, gtserror.Newf("error converting boosted status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set boosted status and set interactions from original.
|
||||||
|
apiStatus.Reblog = &apimodel.StatusReblogged{reblog}
|
||||||
|
apiStatus.Favourited = apiStatus.Reblog.Favourited
|
||||||
|
apiStatus.Bookmarked = apiStatus.Reblog.Bookmarked
|
||||||
|
apiStatus.Muted = apiStatus.Reblog.Muted
|
||||||
|
apiStatus.Reblogged = apiStatus.Reblog.Reblogged
|
||||||
|
apiStatus.Pinned = apiStatus.Reblog.Pinned
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseStatusToFrontend performs the main logic
|
||||||
|
// of statusToFrontend() without handling of boost
|
||||||
|
// logic, to prevent *possible* recursion issues.
|
||||||
|
func (c *Converter) baseStatusToFrontend(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
s *gtsmodel.Status,
|
s *gtsmodel.Status,
|
||||||
requestingAccount *gtsmodel.Account,
|
requestingAccount *gtsmodel.Account,
|
||||||
filterContext statusfilter.FilterContext,
|
filterContext statusfilter.FilterContext,
|
||||||
filters []*gtsmodel.Filter,
|
filters []*gtsmodel.Filter,
|
||||||
mutes *usermute.CompiledUserMuteList,
|
mutes *usermute.CompiledUserMuteList,
|
||||||
) (*apimodel.Status, error) {
|
) (
|
||||||
|
*apimodel.Status,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
// Try to populate status struct pointer fields.
|
// Try to populate status struct pointer fields.
|
||||||
// We can continue in many cases of partial failure,
|
// We can continue in many cases of partial failure,
|
||||||
// but there are some fields we actually need.
|
// but there are some fields we actually need.
|
||||||
if err := c.state.DB.PopulateStatus(ctx, s); err != nil {
|
if err := c.state.DB.PopulateStatus(ctx, s); err != nil {
|
||||||
if s.Account == nil {
|
switch {
|
||||||
err = gtserror.Newf("error(s) populating status, cannot continue (status.Account not set): %w", err)
|
case s.Account == nil:
|
||||||
return nil, err
|
return nil, gtserror.Newf("error(s) populating status, required account not set: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
if s.BoostOfID != "" && s.BoostOf == nil {
|
case s.BoostOfID != "" && s.BoostOf == nil:
|
||||||
err = gtserror.Newf("error(s) populating status, cannot continue (status.BoostOfID set, but status.Boost not set): %w", err)
|
return nil, gtserror.Newf("error(s) populating status, required boost not set: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
default:
|
||||||
log.Errorf(ctx, "error(s) populating status, will continue: %v", err)
|
log.Errorf(ctx, "error(s) populating status, will continue: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apiAuthorAccount, err := c.AccountToAPIAccountPublic(ctx, s.Account)
|
apiAuthorAccount, err := c.AccountToAPIAccountPublic(ctx, s.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1153,19 +1202,6 @@ func (c *Converter) statusToFrontend(
|
||||||
apiStatus.Language = util.Ptr(s.Language)
|
apiStatus.Language = util.Ptr(s.Language)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.BoostOf != nil {
|
|
||||||
reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount, filterContext, filters, mutes)
|
|
||||||
if errors.Is(err, statusfilter.ErrHideStatus) {
|
|
||||||
// If we'd hide the original status, hide the boost.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.Newf("error converting boosted status: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
apiStatus.Reblog = &apimodel.StatusReblogged{reblog}
|
|
||||||
}
|
|
||||||
|
|
||||||
if app := s.CreatedWithApplication; app != nil {
|
if app := s.CreatedWithApplication; app != nil {
|
||||||
apiStatus.Application, err = c.AppToAPIAppPublic(ctx, app)
|
apiStatus.Application, err = c.AppToAPIAppPublic(ctx, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1190,14 +1226,9 @@ func (c *Converter) statusToFrontend(
|
||||||
|
|
||||||
// Status interactions.
|
// Status interactions.
|
||||||
//
|
//
|
||||||
// Take from boosted status if set,
|
if s.BoostOf != nil { //nolint
|
||||||
// otherwise take from status itself.
|
// populated *outside* this
|
||||||
if apiStatus.Reblog != nil {
|
// function to prevent recursion.
|
||||||
apiStatus.Favourited = apiStatus.Reblog.Favourited
|
|
||||||
apiStatus.Bookmarked = apiStatus.Reblog.Bookmarked
|
|
||||||
apiStatus.Muted = apiStatus.Reblog.Muted
|
|
||||||
apiStatus.Reblogged = apiStatus.Reblog.Reblogged
|
|
||||||
apiStatus.Pinned = apiStatus.Reblog.Pinned
|
|
||||||
} else {
|
} else {
|
||||||
interacts, err := c.interactionsWithStatusForAccount(ctx, s, requestingAccount)
|
interacts, err := c.interactionsWithStatusForAccount(ctx, s, requestingAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1230,6 +1261,7 @@ func (c *Converter) statusToFrontend(
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("error applying filters: %w", err)
|
return nil, fmt.Errorf("error applying filters: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus.Filtered = filterResults
|
apiStatus.Filtered = filterResults
|
||||||
|
|
||||||
return apiStatus, nil
|
return apiStatus, nil
|
||||||
|
|
Loading…
Reference in a new issue