mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 07:22:24 -05: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
					
				
			
		
					 7 changed files with 244 additions and 127 deletions
				
			
		|  | @ -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 @@ import ( | ||||||
| 	"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 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"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…
	
	Add table
		Add a link
		
	
		Reference in a new issue