mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:22:25 -05:00 
			
		
		
		
	[bugfix/chore] Announce reliability updates (#2405)
		
	* [bugfix/chore] `Announce` updates * test update * fix tests * TestParseAnnounce * update comments * don't lock/unlock, change function signature * naming stuff * don't check domain block twice * UnwrapIfBoost * beep boop
This commit is contained in:
		
					parent
					
						
							
								d1cac53cbb
							
						
					
				
			
			
				commit
				
					
						0e2c342191
					
				
			
		
					 15 changed files with 425 additions and 285 deletions
				
			
		|  | @ -20,66 +20,107 @@ package dereferencing | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" |  | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 
 | 
 | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (d *Dereferencer) DereferenceAnnounce(ctx context.Context, announce *gtsmodel.Status, requestingUsername string) error { | // EnrichAnnounce enriches the given boost wrapper status | ||||||
| 	if announce.BoostOf == nil { | // by either fetching from the DB or dereferencing the target | ||||||
| 		// we can't do anything unfortunately | // status, populating the boost wrapper's fields based on the | ||||||
| 		return errors.New("DereferenceAnnounce: no URI to dereference") | // target status, and then storing the wrapper in the database. | ||||||
|  | // The wrapper is then returned to the caller. | ||||||
|  | // | ||||||
|  | // The provided boost wrapper status must have BoostOfURI set. | ||||||
|  | func (d *Dereferencer) EnrichAnnounce( | ||||||
|  | 	ctx context.Context, | ||||||
|  | 	boost *gtsmodel.Status, | ||||||
|  | 	requestUser string, | ||||||
|  | ) (*gtsmodel.Status, error) { | ||||||
|  | 	targetURI := boost.BoostOfURI | ||||||
|  | 	if targetURI == "" { | ||||||
|  | 		// We can't do anything. | ||||||
|  | 		return nil, gtserror.Newf("no URI to dereference") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Parse the boosted status' URI | 	// Parse the boost target status URI. | ||||||
| 	boostedURI, err := url.Parse(announce.BoostOf.URI) | 	targetURIObj, err := url.Parse(targetURI) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("DereferenceAnnounce: couldn't parse boosted status URI %s: %s", announce.BoostOf.URI, err) | 		return nil, gtserror.Newf( | ||||||
|  | 			"couldn't parse boost target status URI %s: %w", | ||||||
|  | 			targetURI, err, | ||||||
|  | 		) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check whether the originating status is from a blocked host | 	// Fetch/deref status being boosted. | ||||||
| 	if blocked, err := d.state.DB.IsDomainBlocked(ctx, boostedURI.Host); blocked || err != nil { | 	var target *gtsmodel.Status | ||||||
| 		return fmt.Errorf("DereferenceAnnounce: domain %s is blocked", boostedURI.Host) |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	var boostedStatus *gtsmodel.Status | 	if targetURIObj.Host == config.GetHost() { | ||||||
| 
 |  | ||||||
| 	if boostedURI.Host == config.GetHost() { |  | ||||||
| 		// This is a local status, fetch from the database | 		// This is a local status, fetch from the database | ||||||
| 		status, err := d.state.DB.GetStatusByURI(ctx, boostedURI.String()) | 		target, err = d.state.DB.GetStatusByURI(ctx, targetURI) | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("DereferenceAnnounce: error fetching local status %q: %v", announce.BoostOf.URI, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Set boosted status |  | ||||||
| 		boostedStatus = status |  | ||||||
| 	} else { | 	} else { | ||||||
| 		// This is a boost of a remote status, we need to dereference it. | 		// This is a remote status, we need to dereference it. | ||||||
| 		status, _, err := d.GetStatusByURI(ctx, requestingUsername, boostedURI) | 		// | ||||||
|  | 		// 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 err != nil { | ||||||
| 			return fmt.Errorf("DereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.BoostOf.URI, err) | 		return nil, gtserror.Newf( | ||||||
|  | 			"error getting boost target status %s: %w", | ||||||
|  | 			targetURI, err, | ||||||
|  | 		) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		// Set boosted status | 	// Generate an ID for the boost wrapper status. | ||||||
| 		boostedStatus = status | 	boost.ID, err = id.NewULIDFromTime(boost.CreatedAt) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, gtserror.Newf("error generating id: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	announce.Content = boostedStatus.Content | 	// Populate remaining fields on | ||||||
| 	announce.ContentWarning = boostedStatus.ContentWarning | 	// the boost wrapper using target. | ||||||
| 	announce.ActivityStreamsType = boostedStatus.ActivityStreamsType | 	boost.Content = target.Content | ||||||
| 	announce.Sensitive = boostedStatus.Sensitive | 	boost.ContentWarning = target.ContentWarning | ||||||
| 	announce.Language = boostedStatus.Language | 	boost.ActivityStreamsType = target.ActivityStreamsType | ||||||
| 	announce.Text = boostedStatus.Text | 	boost.Sensitive = target.Sensitive | ||||||
| 	announce.BoostOfID = boostedStatus.ID | 	boost.Language = target.Language | ||||||
| 	announce.BoostOfAccountID = boostedStatus.AccountID | 	boost.Text = target.Text | ||||||
| 	announce.Visibility = boostedStatus.Visibility | 	boost.BoostOfID = target.ID | ||||||
| 	announce.Federated = boostedStatus.Federated | 	boost.BoostOf = target | ||||||
| 	announce.Boostable = boostedStatus.Boostable | 	boost.BoostOfAccountID = target.AccountID | ||||||
| 	announce.Replyable = boostedStatus.Replyable | 	boost.BoostOfAccount = target.Account | ||||||
| 	announce.Likeable = boostedStatus.Likeable | 	boost.Visibility = target.Visibility | ||||||
| 	announce.BoostOf = boostedStatus | 	boost.Federated = target.Federated | ||||||
|  | 	boost.Boostable = target.Boostable | ||||||
|  | 	boost.Replyable = target.Replyable | ||||||
|  | 	boost.Likeable = target.Likeable | ||||||
| 
 | 
 | ||||||
| 	return nil | 	// Store the boost wrapper status. | ||||||
|  | 	switch err = d.state.DB.PutStatus(ctx, boost); { | ||||||
|  | 	case err == nil: | ||||||
|  | 		// All good baby. | ||||||
|  | 
 | ||||||
|  | 	case errors.Is(err, db.ErrAlreadyExists): | ||||||
|  | 		// DATA RACE! We likely lost out to another goroutine | ||||||
|  | 		// in a call to db.Put(Status). Look again in DB by URI. | ||||||
|  | 		boost, err = d.state.DB.GetStatusByURI(ctx, boost.URI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			err = gtserror.Newf( | ||||||
|  | 				"error getting boost wrapper status %s from database after race: %w", | ||||||
|  | 				boost.URI, err, | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	default: | ||||||
|  | 		// Proper database error. | ||||||
|  | 		err = gtserror.Newf("db error inserting status: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return boost, err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -50,8 +50,10 @@ func (suite *AnnounceTestSuite) TestNewAnnounce() { | ||||||
| 	suite.True(ok) | 	suite.True(ok) | ||||||
| 	suite.Equal(announcingAccount.ID, boost.AccountID) | 	suite.Equal(announcingAccount.ID, boost.AccountID) | ||||||
| 
 | 
 | ||||||
| 	// only the URI will be set on the boosted status because it still needs to be dereferenced | 	// only the URI will be set for the boosted status | ||||||
| 	suite.NotEmpty(boost.BoostOf.URI) | 	// because it still needs to be dereferenced | ||||||
|  | 	suite.Nil(boost.BoostOf) | ||||||
|  | 	suite.Equal("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1", boost.BoostOfURI) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *AnnounceTestSuite) TestAnnounceTwice() { | func (suite *AnnounceTestSuite) TestAnnounceTwice() { | ||||||
|  | @ -81,8 +83,10 @@ func (suite *AnnounceTestSuite) TestAnnounceTwice() { | ||||||
| 		return nil | 		return nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	// only the URI will be set on the boosted status because it still needs to be dereferenced | 	// only the URI will be set for the boosted status | ||||||
| 	suite.NotEmpty(boost.BoostOf.URI) | 	// because it still needs to be dereferenced | ||||||
|  | 	suite.Nil(boost.BoostOf) | ||||||
|  | 	suite.Equal("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1", boost.BoostOfURI) | ||||||
| 
 | 
 | ||||||
| 	ctx2 := createTestContext(receivingAccount2, announcingAccount) | 	ctx2 := createTestContext(receivingAccount2, announcingAccount) | ||||||
| 	announce2 := suite.testActivities["announce_forwarded_1_turtle"] | 	announce2 := suite.testActivities["announce_forwarded_1_turtle"] | ||||||
|  |  | ||||||
|  | @ -50,6 +50,7 @@ type Status struct { | ||||||
| 	InReplyTo                *Status            `bun:"-"`                                                           // status corresponding to inReplyToID | 	InReplyTo                *Status            `bun:"-"`                                                           // status corresponding to inReplyToID | ||||||
| 	InReplyToAccount         *Account           `bun:"rel:belongs-to"`                                              // account corresponding to inReplyToAccountID | 	InReplyToAccount         *Account           `bun:"rel:belongs-to"`                                              // account corresponding to inReplyToAccountID | ||||||
| 	BoostOfID                string             `bun:"type:CHAR(26),nullzero"`                                      // id of the status this status is a boost of | 	BoostOfID                string             `bun:"type:CHAR(26),nullzero"`                                      // id of the status this status is a boost of | ||||||
|  | 	BoostOfURI               string             `bun:"-"`                                                           // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes. | ||||||
| 	BoostOfAccountID         string             `bun:"type:CHAR(26),nullzero"`                                      // id of the account that owns the boosted status | 	BoostOfAccountID         string             `bun:"type:CHAR(26),nullzero"`                                      // id of the account that owns the boosted status | ||||||
| 	BoostOf                  *Status            `bun:"-"`                                                           // status that corresponds to boostOfID | 	BoostOf                  *Status            `bun:"-"`                                                           // status that corresponds to boostOfID | ||||||
| 	BoostOfAccount           *Account           `bun:"rel:belongs-to"`                                              // account that corresponds to boostOfAccountID | 	BoostOfAccount           *Account           `bun:"rel:belongs-to"`                                              // account that corresponds to boostOfAccountID | ||||||
|  |  | ||||||
|  | @ -119,6 +119,25 @@ func (p *Processor) GetVisibleTargetStatus( | ||||||
| 	return target, nil | 	return target, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // UnwrapIfBoost "unwraps" the given status if | ||||||
|  | // it's a boost wrapper, by returning the boosted | ||||||
|  | // status it targets (pending visibility checks). | ||||||
|  | // | ||||||
|  | // Just returns the input status if it's not a boost. | ||||||
|  | func (p *Processor) UnwrapIfBoost( | ||||||
|  | 	ctx context.Context, | ||||||
|  | 	requester *gtsmodel.Account, | ||||||
|  | 	status *gtsmodel.Status, | ||||||
|  | ) (*gtsmodel.Status, gtserror.WithCode) { | ||||||
|  | 	if status.BoostOfID == "" { | ||||||
|  | 		return status, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return p.GetVisibleTargetStatus(ctx, | ||||||
|  | 		requester, status.BoostOfID, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // GetAPIStatus fetches the appropriate API status model for target. | // GetAPIStatus fetches the appropriate API status model for target. | ||||||
| func (p *Processor) GetAPIStatus( | func (p *Processor) GetAPIStatus( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
|  |  | ||||||
|  | @ -51,6 +51,11 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI | ||||||
| 		return nil, gtserror.NewErrorNotFound(errors.New(text)) | 		return nil, gtserror.NewErrorNotFound(errors.New(text)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if status.BoostOfID != "" { | ||||||
|  | 		const text = "status is a boost wrapper" | ||||||
|  | 		return nil, gtserror.NewErrorNotFound(errors.New(text)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	visible, err := p.filter.StatusVisible(ctx, requester, status) | 	visible, err := p.filter.StatusVisible(ctx, requester, status) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, gtserror.NewErrorInternalError(err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | @ -106,6 +111,11 @@ func (p *Processor) StatusRepliesGet( | ||||||
| 		return nil, gtserror.NewErrorNotFound(errors.New(text)) | 		return nil, gtserror.NewErrorNotFound(errors.New(text)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if status.BoostOfID != "" { | ||||||
|  | 		const text = "status is a boost wrapper" | ||||||
|  | 		return nil, gtserror.NewErrorNotFound(errors.New(text)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Parse replies collection ID from status' URI with onlyOtherAccounts param. | 	// Parse replies collection ID from status' URI with onlyOtherAccounts param. | ||||||
| 	onlyOtherAccStr := "only_other_accounts=" + strconv.FormatBool(onlyOtherAccounts) | 	onlyOtherAccStr := "only_other_accounts=" + strconv.FormatBool(onlyOtherAccounts) | ||||||
| 	collectionID, err := url.Parse(status.URI + "/replies?" + onlyOtherAccStr) | 	collectionID, err := url.Parse(status.URI + "/replies?" + onlyOtherAccStr) | ||||||
|  |  | ||||||
|  | @ -30,106 +30,125 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/messages" | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // BoostCreate processes the boost/reblog of a given status, returning the newly-created boost if all is well. | // BoostCreate processes the boost/reblog of target | ||||||
| func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { | // status, returning the newly-created boost. | ||||||
| 	targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID) | func (p *Processor) BoostCreate( | ||||||
| 	if err != nil { | 	ctx context.Context, | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) | 	requester *gtsmodel.Account, | ||||||
| 	} | 	application *gtsmodel.Application, | ||||||
| 	if targetStatus.Account == nil { | 	targetID string, | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) | ) (*apimodel.Status, gtserror.WithCode) { | ||||||
|  | 	// Get target status and ensure it's not a boost. | ||||||
|  | 	target, errWithCode := p.c.GetVisibleTargetStatus( | ||||||
|  | 		ctx, | ||||||
|  | 		requester, | ||||||
|  | 		targetID, | ||||||
|  | 	) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		return nil, errWithCode | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// if targetStatusID refers to a boost, then we should redirect | 	target, errWithCode = p.c.UnwrapIfBoost( | ||||||
| 	// the target to being the status that was boosted; if we don't | 		ctx, | ||||||
| 	// do this, then we end up in weird situations where people | 		requester, | ||||||
| 	// boost boosts, and it looks absolutely bizarre in the UI | 		target, | ||||||
| 	if targetStatus.BoostOfID != "" { | 	) | ||||||
| 		if targetStatus.BoostOf == nil { | 	if errWithCode != nil { | ||||||
| 			b, err := p.state.DB.GetStatusByID(ctx, targetStatus.BoostOfID) | 		return nil, errWithCode | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, gtserror.NewErrorNotFound(fmt.Errorf("couldn't fetch boosted status %s", targetStatus.BoostOfID)) |  | ||||||
| 			} |  | ||||||
| 			targetStatus.BoostOf = b |  | ||||||
| 		} |  | ||||||
| 		targetStatus = targetStatus.BoostOf |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	boostable, err := p.filter.StatusBoostable(ctx, requestingAccount, targetStatus) | 	// Ensure valid boost target. | ||||||
|  | 	boostable, err := p.filter.StatusBoostable(ctx, | ||||||
|  | 		requester, | ||||||
|  | 		target, | ||||||
|  | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is boostable: %s", targetStatus.ID, err)) | 		err := gtserror.Newf("error seeing if status %s is boostable: %w", target.ID, err) | ||||||
| 	} else if !boostable { | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 		return nil, gtserror.NewErrorNotFound(errors.New("status is not boostable")) |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// it's visible! it's boostable! so let's boost the FUCK out of it | 	if !boostable { | ||||||
| 	boostWrapperStatus, err := p.converter.StatusToBoost(ctx, targetStatus, requestingAccount) | 		err := gtserror.New("status is not boostable") | ||||||
|  | 		return nil, gtserror.NewErrorNotFound(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Status is visible and boostable. | ||||||
|  | 	boost, err := p.converter.StatusToBoost(ctx, | ||||||
|  | 		target, | ||||||
|  | 		requester, | ||||||
|  | 		application.ID, | ||||||
|  | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, gtserror.NewErrorInternalError(err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	boostWrapperStatus.CreatedWithApplicationID = application.ID | 	// Store the new boost. | ||||||
| 	boostWrapperStatus.BoostOfAccount = targetStatus.Account | 	if err := p.state.DB.PutStatus(ctx, boost); err != nil { | ||||||
| 
 |  | ||||||
| 	// put the boost in the database |  | ||||||
| 	if err := p.state.DB.PutStatus(ctx, boostWrapperStatus); err != nil { |  | ||||||
| 		return nil, gtserror.NewErrorInternalError(err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// send it back to the processor for async processing | 	// Process side effects asynchronously. | ||||||
| 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | ||||||
| 		APObjectType:   ap.ActivityAnnounce, | 		APObjectType:   ap.ActivityAnnounce, | ||||||
| 		APActivityType: ap.ActivityCreate, | 		APActivityType: ap.ActivityCreate, | ||||||
| 		GTSModel:       boostWrapperStatus, | 		GTSModel:       boost, | ||||||
| 		OriginAccount:  requestingAccount, | 		OriginAccount:  requester, | ||||||
| 		TargetAccount:  targetStatus.Account, | 		TargetAccount:  target.Account, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	return p.c.GetAPIStatus(ctx, requestingAccount, boostWrapperStatus) | 	return p.c.GetAPIStatus(ctx, requester, boost) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well. | // BoostRemove processes the unboost/unreblog of | ||||||
| func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { | // target status, returning the target status. | ||||||
| 	targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID) | func (p *Processor) BoostRemove( | ||||||
| 	if err != nil { | 	ctx context.Context, | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) | 	requester *gtsmodel.Account, | ||||||
| 	} | 	application *gtsmodel.Application, | ||||||
| 	if targetStatus.Account == nil { | 	targetID string, | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) | ) (*apimodel.Status, gtserror.WithCode) { | ||||||
|  | 	// Get target status and ensure it's not a boost. | ||||||
|  | 	target, errWithCode := p.c.GetVisibleTargetStatus( | ||||||
|  | 		ctx, | ||||||
|  | 		requester, | ||||||
|  | 		targetID, | ||||||
|  | 	) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		return nil, errWithCode | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus) | 	target, errWithCode = p.c.UnwrapIfBoost( | ||||||
| 	if err != nil { | 		ctx, | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) | 		requester, | ||||||
| 	} | 		target, | ||||||
| 	if !visible { | 	) | ||||||
| 		return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) | 	if errWithCode != nil { | ||||||
|  | 		return nil, errWithCode | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check whether the requesting account has boosted the given status ID. | 	// Check whether requester has actually | ||||||
| 	boost, err := p.state.DB.GetStatusBoost(ctx, targetStatusID, requestingAccount.ID) | 	// boosted target, by trying to get the boost. | ||||||
| 	if err != nil { | 	boost, err := p.state.DB.GetStatusBoost(ctx, | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error checking status boost %s: %w", targetStatusID, err)) | 		target.ID, | ||||||
|  | 		requester.ID, | ||||||
|  | 	) | ||||||
|  | 	if err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		err = gtserror.Newf("db error getting boost of %s: %w", target.ID, err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if boost != nil { | 	if boost != nil { | ||||||
| 		// pin some stuff onto the boost while we have it out of the db | 		// Status was boosted. Process unboost side effects asynchronously. | ||||||
| 		boost.Account = requestingAccount |  | ||||||
| 		boost.BoostOf = targetStatus |  | ||||||
| 		boost.BoostOfAccount = targetStatus.Account |  | ||||||
| 		boost.BoostOf.Account = targetStatus.Account |  | ||||||
| 
 |  | ||||||
| 		// send it back to the processor for async processing |  | ||||||
| 		p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | 		p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | ||||||
| 			APObjectType:   ap.ActivityAnnounce, | 			APObjectType:   ap.ActivityAnnounce, | ||||||
| 			APActivityType: ap.ActivityUndo, | 			APActivityType: ap.ActivityUndo, | ||||||
| 			GTSModel:       boost, | 			GTSModel:       boost, | ||||||
| 			OriginAccount:  requestingAccount, | 			OriginAccount:  requester, | ||||||
| 			TargetAccount:  targetStatus.Account, | 			TargetAccount:  target.Account, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus) | 	return p.c.GetAPIStatus(ctx, requester, target) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. | // StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. | ||||||
|  |  | ||||||
|  | @ -33,24 +33,46 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (p *Processor) getFaveableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) { | func (p *Processor) getFaveableStatus( | ||||||
| 	targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID) | 	ctx context.Context, | ||||||
|  | 	requester *gtsmodel.Account, | ||||||
|  | 	targetID string, | ||||||
|  | ) ( | ||||||
|  | 	*gtsmodel.Status, | ||||||
|  | 	*gtsmodel.StatusFave, | ||||||
|  | 	gtserror.WithCode, | ||||||
|  | ) { | ||||||
|  | 	// Get target status and ensure it's not a boost. | ||||||
|  | 	target, errWithCode := p.c.GetVisibleTargetStatus( | ||||||
|  | 		ctx, | ||||||
|  | 		requester, | ||||||
|  | 		targetID, | ||||||
|  | 	) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		return nil, nil, errWithCode | 		return nil, nil, errWithCode | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !*targetStatus.Likeable { | 	target, errWithCode = p.c.UnwrapIfBoost( | ||||||
|  | 		ctx, | ||||||
|  | 		requester, | ||||||
|  | 		target, | ||||||
|  | 	) | ||||||
|  | 	if errWithCode != nil { | ||||||
|  | 		return nil, nil, errWithCode | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !*target.Likeable { | ||||||
| 		err := errors.New("status is not faveable") | 		err := errors.New("status is not faveable") | ||||||
| 		return nil, nil, gtserror.NewErrorForbidden(err, err.Error()) | 		return nil, nil, gtserror.NewErrorForbidden(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fave, err := p.state.DB.GetStatusFave(ctx, requestingAccount.ID, targetStatusID) | 	fave, err := p.state.DB.GetStatusFave(ctx, requester.ID, target.ID) | ||||||
| 	if err != nil && !errors.Is(err, db.ErrNoEntries) { | 	if err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
| 		err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err) | 		err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err) | ||||||
| 		return nil, nil, gtserror.NewErrorInternalError(err) | 		return nil, nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return targetStatus, fave, nil | 	return target, fave, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists). | // FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists). | ||||||
|  |  | ||||||
|  | @ -26,7 +26,6 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | 	"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/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/messages" | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/processing/account" | 	"github.com/superseriousbusiness/gotosocial/internal/processing/account" | ||||||
|  | @ -332,45 +331,44 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg messages.FromFediAPI) err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI) error { | func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI) error { | ||||||
| 	status, ok := fMsg.GTSModel.(*gtsmodel.Status) | 	boost, ok := fMsg.GTSModel.(*gtsmodel.Status) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel) | 		return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Dereference status that this boosts, note | 	// Dereference status that this boosts, note | ||||||
| 	// that this will handle dereferencing the status | 	// that this will handle storing the boost in | ||||||
|  | 	// the db, and dereferencing the target status | ||||||
| 	// ancestors / descendants where appropriate. | 	// ancestors / descendants where appropriate. | ||||||
| 	if err := p.federate.DereferenceAnnounce(ctx, | 	var err error | ||||||
| 		status, | 	boost, err = p.federate.EnrichAnnounce( | ||||||
|  | 		ctx, | ||||||
|  | 		boost, | ||||||
| 		fMsg.ReceivingAccount.Username, | 		fMsg.ReceivingAccount.Username, | ||||||
| 	); err != nil { | 	) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if gtserror.IsUnretrievable(err) { | ||||||
|  | 			// Boosted status domain blocked, nothing to do. | ||||||
|  | 			log.Debugf(ctx, "skipping announce: %v", err) | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Actual error. | ||||||
| 		return gtserror.Newf("error dereferencing announce: %w", err) | 		return gtserror.Newf("error dereferencing announce: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Generate an ID for the boost wrapper status. |  | ||||||
| 	statusID, err := id.NewULIDFromTime(status.CreatedAt) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return gtserror.Newf("error generating id: %w", err) |  | ||||||
| 	} |  | ||||||
| 	status.ID = statusID |  | ||||||
| 
 |  | ||||||
| 	// Store the boost wrapper status. |  | ||||||
| 	if err := p.state.DB.PutStatus(ctx, status); err != nil { |  | ||||||
| 		return gtserror.Newf("db error inserting status: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Timeline and notify the announce. | 	// Timeline and notify the announce. | ||||||
| 	if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil { | 	if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil { | ||||||
| 		log.Errorf(ctx, "error timelining and notifying status: %v", err) | 		log.Errorf(ctx, "error timelining and notifying status: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := p.surface.notifyAnnounce(ctx, status); err != nil { | 	if err := p.surface.notifyAnnounce(ctx, boost); err != nil { | ||||||
| 		log.Errorf(ctx, "error notifying announce: %v", err) | 		log.Errorf(ctx, "error notifying announce: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Interaction counts changed on the original status; | 	// Interaction counts changed on the original status; | ||||||
| 	// uncache the prepared version from all timelines. | 	// uncache the prepared version from all timelines. | ||||||
| 	p.surface.invalidateStatusFromTimelines(ctx, status.ID) | 	p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID) | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -45,9 +45,7 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() { | ||||||
| 	boostingAccount := suite.testAccounts["remote_account_1"] | 	boostingAccount := suite.testAccounts["remote_account_1"] | ||||||
| 	announceStatus := >smodel.Status{} | 	announceStatus := >smodel.Status{} | ||||||
| 	announceStatus.URI = "https://example.org/some-announce-uri" | 	announceStatus.URI = "https://example.org/some-announce-uri" | ||||||
| 	announceStatus.BoostOf = >smodel.Status{ | 	announceStatus.BoostOfURI = boostedStatus.URI | ||||||
| 		URI: boostedStatus.URI, |  | ||||||
| 	} |  | ||||||
| 	announceStatus.CreatedAt = time.Now() | 	announceStatus.CreatedAt = time.Now() | ||||||
| 	announceStatus.UpdatedAt = time.Now() | 	announceStatus.UpdatedAt = time.Now() | ||||||
| 	announceStatus.AccountID = boostingAccount.ID | 	announceStatus.AccountID = boostingAccount.ID | ||||||
|  |  | ||||||
|  | @ -527,29 +527,15 @@ func (c *Converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable) | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ASAnnounceToStatus converts an activitystreams 'announce' into a status. | // ASAnnounceToStatus converts an activitystreams 'announce' into a boost | ||||||
| // | // wrapper status. The returned bool indicates whether this boost is new | ||||||
| // The returned bool indicates whether this status is new (true) or not new (false). | // (true) or not. If new, callers should use `status.BoostOfURI` to see the | ||||||
| // | // status being boosted, and do dereferencing on it as appropriate. If not | ||||||
| // In other words, if the status is already in the database with the ID set on the announceable, then that will be returned, | // new, then the boost has already been fully processed and can be ignored. | ||||||
| // the returned bool will be false, and no further processing is necessary. If the returned bool is true, indicating | func (c *Converter) ASAnnounceToStatus( | ||||||
| // that this is a new announce, then further processing will be necessary, because the returned status will be bareboned and | 	ctx context.Context, | ||||||
| // require further dereferencing. | 	announceable ap.Announceable, | ||||||
| // | ) (*gtsmodel.Status, bool, error) { | ||||||
| // This is useful when multiple users on an instance might receive the same boost, and we only want to process the boost once. |  | ||||||
| // |  | ||||||
| // NOTE -- this is different from one status being boosted multiple times! In this case, new boosts should indeed be created. |  | ||||||
| // |  | ||||||
| // Implementation note: this function creates and returns a boost WRAPPER |  | ||||||
| // status which references the boosted status in its BoostOf field. No |  | ||||||
| // dereferencing is done on the boosted status by this function. Callers |  | ||||||
| // should look at `status.BoostOf` to see the status being boosted, and do |  | ||||||
| // dereferencing on it as appropriate. |  | ||||||
| // |  | ||||||
| // The returned boolean indicates whether or not the boost has already been |  | ||||||
| // seen before by this instance. If it was, then status.BoostOf should be a |  | ||||||
| // fully filled-out status. If not, then only status.BoostOf.URI will be set. |  | ||||||
| func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Announceable) (*gtsmodel.Status, bool, error) { |  | ||||||
| 	// Default assume | 	// Default assume | ||||||
| 	// we already have. | 	// we already have. | ||||||
| 	isNew := false | 	isNew := false | ||||||
|  | @ -565,21 +551,21 @@ func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno | ||||||
| 	uri := uriObj.String() | 	uri := uriObj.String() | ||||||
| 
 | 
 | ||||||
| 	// Check if we already have this boost in the database. | 	// Check if we already have this boost in the database. | ||||||
| 	status, err := c.state.DB.GetStatusByURI(ctx, uri) | 	boost, err := c.state.DB.GetStatusByURI(ctx, uri) | ||||||
| 	if err != nil && !errors.Is(err, db.ErrNoEntries) { | 	if err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
| 		err = gtserror.Newf("db error trying to get status with uri %s: %w", uri, err) | 		err = gtserror.Newf("db error trying to get status with uri %s: %w", uri, err) | ||||||
| 		return nil, isNew, err | 		return nil, isNew, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if status != nil { | 	if boost != nil { | ||||||
| 		// We already have this status, | 		// We already have this boost, | ||||||
| 		// no need to proceed further. | 		// no need to proceed further. | ||||||
| 		return status, isNew, nil | 		return boost, isNew, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Create DB status with URI | 	// Create boost with URI | ||||||
| 	status = new(gtsmodel.Status) | 	boost = new(gtsmodel.Status) | ||||||
| 	status.URI = uri | 	boost.URI = uri | ||||||
| 	isNew = true | 	isNew = true | ||||||
| 
 | 
 | ||||||
| 	// Get the URI of the boosted status. | 	// Get the URI of the boosted status. | ||||||
|  | @ -590,22 +576,21 @@ func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Set the URI of the boosted status on | 	// Set the URI of the boosted status on | ||||||
| 	// the new status, for later dereferencing. | 	// the boost, for later dereferencing. | ||||||
| 	status.BoostOf = new(gtsmodel.Status) | 	boost.BoostOfURI = boostOf[0].String() | ||||||
| 	status.BoostOf.URI = boostOf[0].String() |  | ||||||
| 
 | 
 | ||||||
| 	// Extract published time for the boost, | 	// Extract published time for the boost, | ||||||
| 	// zero-time will fall back to db defaults. | 	// zero-time will fall back to db defaults. | ||||||
| 	if pub := ap.GetPublished(announceable); !pub.IsZero() { | 	if pub := ap.GetPublished(announceable); !pub.IsZero() { | ||||||
| 		status.CreatedAt = pub | 		boost.CreatedAt = pub | ||||||
| 		status.UpdatedAt = pub | 		boost.UpdatedAt = pub | ||||||
| 	} else { | 	} else { | ||||||
| 		log.Warnf(ctx, "unusable published property on %s", uri) | 		log.Warnf(ctx, "unusable published property on %s", uri) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Extract and load the boost actor account, | 	// Extract and load the boost actor account, | ||||||
| 	// (this MUST already be in database by now). | 	// (this MUST already be in database by now). | ||||||
| 	status.Account, err = c.getASActorAccount(ctx, | 	boost.Account, err = c.getASActorAccount(ctx, | ||||||
| 		uri, | 		uri, | ||||||
| 		announceable, | 		announceable, | ||||||
| 	) | 	) | ||||||
|  | @ -614,13 +599,13 @@ func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Set the related status<->account fields. | 	// Set the related status<->account fields. | ||||||
| 	status.AccountURI = status.Account.URI | 	boost.AccountURI = boost.Account.URI | ||||||
| 	status.AccountID = status.Account.ID | 	boost.AccountID = boost.Account.ID | ||||||
| 
 | 
 | ||||||
| 	// Calculate intended visibility of the boost. | 	// Calculate intended visibility of the boost. | ||||||
| 	status.Visibility, err = ap.ExtractVisibility( | 	boost.Visibility, err = ap.ExtractVisibility( | ||||||
| 		announceable, | 		announceable, | ||||||
| 		status.Account.FollowersURI, | 		boost.Account.FollowersURI, | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err := gtserror.Newf("error extracting status visibility for %s: %w", uri, err) | 		err := gtserror.Newf("error extracting status visibility for %s: %w", uri, err) | ||||||
|  | @ -629,15 +614,15 @@ func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno | ||||||
| 
 | 
 | ||||||
| 	// Below IDs will all be included in the | 	// Below IDs will all be included in the | ||||||
| 	// boosted status, so set them empty here. | 	// boosted status, so set them empty here. | ||||||
| 	status.AttachmentIDs = make([]string, 0) | 	boost.AttachmentIDs = make([]string, 0) | ||||||
| 	status.TagIDs = make([]string, 0) | 	boost.TagIDs = make([]string, 0) | ||||||
| 	status.MentionIDs = make([]string, 0) | 	boost.MentionIDs = make([]string, 0) | ||||||
| 	status.EmojiIDs = make([]string, 0) | 	boost.EmojiIDs = make([]string, 0) | ||||||
| 
 | 
 | ||||||
| 	// Remaining fields on the boost status will be taken | 	// Remaining fields on the boost will be | ||||||
| 	// from the boosted status; it's not our job to do all | 	// taken from the target status; it's not | ||||||
| 	// that dereferencing here. | 	// our job to do all that dereferencing here. | ||||||
| 	return status, isNew, nil | 	return boost, isNew, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ASFlagToReport converts a remote activitystreams 'flag' representation into a gts model report. | // ASFlagToReport converts a remote activitystreams 'flag' representation into a gts model report. | ||||||
|  |  | ||||||
|  | @ -463,6 +463,46 @@ func (suite *ASToInternalTestSuite) TestParseFlag6() { | ||||||
| 	suite.Equal(report.Comment, "misinformation") | 	suite.Equal(report.Comment, "misinformation") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (suite *ASToInternalTestSuite) TestParseAnnounce() { | ||||||
|  | 	// Boost a status that belongs to a local account | ||||||
|  | 	boostingAccount := suite.testAccounts["remote_account_1"] | ||||||
|  | 	targetStatus := suite.testStatuses["local_account_2_status_1"] | ||||||
|  | 	receivingAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	raw := `{ | ||||||
|  |   "@context": "https://www.w3.org/ns/activitystreams", | ||||||
|  |   "actor": "` + boostingAccount.URI + `", | ||||||
|  |   "id": "http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d", | ||||||
|  |   "object": ["` + targetStatus.URI + `"], | ||||||
|  |   "type": "Announce", | ||||||
|  |   "to": "` + receivingAccount.URI + `" | ||||||
|  |   }` | ||||||
|  | 
 | ||||||
|  | 	t := suite.jsonToType(raw) | ||||||
|  | 	asAnnounce, ok := t.(ap.Announceable) | ||||||
|  | 	if !ok { | ||||||
|  | 		suite.FailNow("type not coercible") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	boost, isNew, err := suite.typeconverter.ASAnnounceToStatus(context.Background(), asAnnounce) | ||||||
|  | 	if err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	suite.True(isNew) | ||||||
|  | 	suite.NotNil(boost) | ||||||
|  | 	suite.Equal(boostingAccount.ID, boost.AccountID) | ||||||
|  | 	suite.NotNil(boost.Account) | ||||||
|  | 
 | ||||||
|  | 	// Of the 'BoostOf' fields, only BoostOfURI will be set. | ||||||
|  | 	// Others are set in dereferencing.EnrichAnnounceSafely. | ||||||
|  | 	suite.Equal(targetStatus.URI, boost.BoostOfURI) | ||||||
|  | 	suite.Empty(boost.BoostOfID) | ||||||
|  | 	suite.Nil(boost.BoostOf) | ||||||
|  | 	suite.Empty(boost.BoostOfAccountID) | ||||||
|  | 	suite.Nil(boost.BoostOfAccount) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestASToInternalTestSuite(t *testing.T) { | func TestASToInternalTestSuite(t *testing.T) { | ||||||
| 	suite.Run(t, new(ASToInternalTestSuite)) | 	suite.Run(t, new(ASToInternalTestSuite)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,90 +19,86 @@ package typeutils | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"time" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FollowRequestToFollow just converts a follow request into a follow, that's it! No bells and whistles. | // FollowRequestToFollow just converts a follow request | ||||||
| func (c *Converter) FollowRequestToFollow(ctx context.Context, f *gtsmodel.FollowRequest) *gtsmodel.Follow { | // into a follow, that's it! No bells and whistles. | ||||||
| 	showReblogs := *f.ShowReblogs | func (c *Converter) FollowRequestToFollow( | ||||||
| 	notify := *f.Notify | 	ctx context.Context, | ||||||
|  | 	fr *gtsmodel.FollowRequest, | ||||||
|  | ) *gtsmodel.Follow { | ||||||
| 	return >smodel.Follow{ | 	return >smodel.Follow{ | ||||||
| 		ID:              f.ID, | 		ID:              fr.ID, | ||||||
| 		CreatedAt:       f.CreatedAt, | 		CreatedAt:       fr.CreatedAt, | ||||||
| 		UpdatedAt:       f.UpdatedAt, | 		UpdatedAt:       fr.UpdatedAt, | ||||||
| 		AccountID:       f.AccountID, | 		AccountID:       fr.AccountID, | ||||||
| 		TargetAccountID: f.TargetAccountID, | 		TargetAccountID: fr.TargetAccountID, | ||||||
| 		ShowReblogs:     &showReblogs, | 		ShowReblogs:     util.Ptr(*fr.ShowReblogs), | ||||||
| 		URI:             f.URI, | 		URI:             fr.URI, | ||||||
| 		Notify:          ¬ify, | 		Notify:          util.Ptr(*fr.Notify), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // StatusToBoost wraps the given status into a boosting status. | // StatusToBoost wraps the target status into a | ||||||
| func (c *Converter) StatusToBoost(ctx context.Context, s *gtsmodel.Status, boostingAccount *gtsmodel.Account) (*gtsmodel.Status, error) { | // boost wrapper status owned by the requester. | ||||||
| 	// the wrapper won't use the same ID as the boosted status so we generate some new UUIDs | func (c *Converter) StatusToBoost( | ||||||
| 	accountURIs := uris.GenerateURIsForAccount(boostingAccount.Username) | 	ctx context.Context, | ||||||
| 	boostWrapperStatusID := id.NewULID() | 	target *gtsmodel.Status, | ||||||
| 	boostWrapperStatusURI := accountURIs.StatusesURI + "/" + boostWrapperStatusID | 	booster *gtsmodel.Account, | ||||||
| 	boostWrapperStatusURL := accountURIs.StatusesURL + "/" + boostWrapperStatusID | 	applicationID string, | ||||||
|  | ) (*gtsmodel.Status, error) { | ||||||
|  | 	// The boost won't use the same IDs as the | ||||||
|  | 	// target so we need to generate new ones. | ||||||
|  | 	boostID := id.NewULID() | ||||||
|  | 	accountURIs := uris.GenerateURIsForAccount(booster.Username) | ||||||
| 
 | 
 | ||||||
| 	local := true | 	boost := >smodel.Status{ | ||||||
| 	if boostingAccount.Domain != "" { | 		ID:  boostID, | ||||||
| 		local = false | 		URI: accountURIs.StatusesURI + "/" + boostID, | ||||||
| 	} | 		URL: accountURIs.StatusesURL + "/" + boostID, | ||||||
| 
 | 
 | ||||||
| 	sensitive := *s.Sensitive | 		// Inherit some fields from the booster account. | ||||||
| 	federated := *s.Federated | 		Local:                    util.Ptr(booster.IsLocal()), | ||||||
| 	boostable := *s.Boostable | 		AccountID:                booster.ID, | ||||||
| 	replyable := *s.Replyable | 		Account:                  booster, | ||||||
| 	likeable := *s.Likeable | 		AccountURI:               booster.URI, | ||||||
|  | 		CreatedWithApplicationID: applicationID, | ||||||
| 
 | 
 | ||||||
| 	boostWrapperStatus := >smodel.Status{ | 		// Replies can be boosted, but | ||||||
| 		ID:  boostWrapperStatusID, | 		// boosts are never replies. | ||||||
| 		URI: boostWrapperStatusURI, |  | ||||||
| 		URL: boostWrapperStatusURL, |  | ||||||
| 
 |  | ||||||
| 		// the boosted status is not created now, but the boost certainly is |  | ||||||
| 		CreatedAt:  time.Now(), |  | ||||||
| 		UpdatedAt:  time.Now(), |  | ||||||
| 		Local:      &local, |  | ||||||
| 		AccountID:  boostingAccount.ID, |  | ||||||
| 		AccountURI: boostingAccount.URI, |  | ||||||
| 
 |  | ||||||
| 		// replies can be boosted, but boosts are never replies |  | ||||||
| 		InReplyToID:        "", | 		InReplyToID:        "", | ||||||
| 		InReplyToAccountID: "", | 		InReplyToAccountID: "", | ||||||
| 
 | 
 | ||||||
| 		// these will all be wrapped in the boosted status so set them empty here | 		// These will all be wrapped in the | ||||||
|  | 		// boosted status so set them empty. | ||||||
| 		AttachmentIDs: []string{}, | 		AttachmentIDs: []string{}, | ||||||
| 		TagIDs:        []string{}, | 		TagIDs:        []string{}, | ||||||
| 		MentionIDs:    []string{}, | 		MentionIDs:    []string{}, | ||||||
| 		EmojiIDs:      []string{}, | 		EmojiIDs:      []string{}, | ||||||
| 
 | 
 | ||||||
| 		// the below fields will be taken from the target status | 		// Remaining fields all | ||||||
| 		Content:             s.Content, | 		// taken from boosted status. | ||||||
| 		ContentWarning:      s.ContentWarning, | 		Content:             target.Content, | ||||||
| 		ActivityStreamsType: s.ActivityStreamsType, | 		ContentWarning:      target.ContentWarning, | ||||||
| 		Sensitive:           &sensitive, | 		ActivityStreamsType: target.ActivityStreamsType, | ||||||
| 		Language:            s.Language, | 		Sensitive:           util.Ptr(*target.Sensitive), | ||||||
| 		Text:                s.Text, | 		Language:            target.Language, | ||||||
| 		BoostOfID:           s.ID, | 		Text:                target.Text, | ||||||
| 		BoostOfAccountID:    s.AccountID, | 		BoostOfID:           target.ID, | ||||||
| 		Visibility:          s.Visibility, | 		BoostOf:             target, | ||||||
| 		Federated:           &federated, | 		BoostOfAccountID:    target.AccountID, | ||||||
| 		Boostable:           &boostable, | 		BoostOfAccount:      target.Account, | ||||||
| 		Replyable:           &replyable, | 		Visibility:          target.Visibility, | ||||||
| 		Likeable:            &likeable, | 		Federated:           util.Ptr(*target.Federated), | ||||||
| 
 | 		Boostable:           util.Ptr(*target.Boostable), | ||||||
| 		// attach these here for convenience -- the boosted status/account won't go in the DB | 		Replyable:           util.Ptr(*target.Replyable), | ||||||
| 		// but they're needed in the processor and for the frontend. Since we have them, we can | 		Likeable:            util.Ptr(*target.Likeable), | ||||||
| 		// attach them so we don't need to fetch them again later (save some DB calls) |  | ||||||
| 		BoostOf: s, |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return boostWrapperStatus, nil | 	return boost, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -724,10 +724,11 @@ func (suite *InternalToASTestSuite) TestSelfBoostFollowersOnlyToAS() { | ||||||
| 	testStatus := suite.testStatuses["local_account_1_status_5"] | 	testStatus := suite.testStatuses["local_account_1_status_5"] | ||||||
| 	testAccount := suite.testAccounts["local_account_1"] | 	testAccount := suite.testAccounts["local_account_1"] | ||||||
| 
 | 
 | ||||||
| 	boostWrapperStatus, err := suite.typeconverter.StatusToBoost(ctx, testStatus, testAccount) | 	boostWrapperStatus, err := suite.typeconverter.StatusToBoost(ctx, testStatus, testAccount, "") | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.NotNil(boostWrapperStatus) | 	suite.NotNil(boostWrapperStatus) | ||||||
| 
 | 
 | ||||||
|  | 	// Set some fields to predictable values for the test. | ||||||
| 	boostWrapperStatus.ID = "01G74JJ1KS331G2JXHRMZCE0ER" | 	boostWrapperStatus.ID = "01G74JJ1KS331G2JXHRMZCE0ER" | ||||||
| 	boostWrapperStatus.URI = "http://localhost:8080/users/the_mighty_zork/statuses/01G74JJ1KS331G2JXHRMZCE0ER" | 	boostWrapperStatus.URI = "http://localhost:8080/users/the_mighty_zork/statuses/01G74JJ1KS331G2JXHRMZCE0ER" | ||||||
| 	boostWrapperStatus.CreatedAt = testrig.TimeMustParse("2022-06-09T13:12:00Z") | 	boostWrapperStatus.CreatedAt = testrig.TimeMustParse("2022-06-09T13:12:00Z") | ||||||
|  |  | ||||||
|  | @ -733,11 +733,18 @@ func (c *Converter) statusToFrontend( | ||||||
| 	s *gtsmodel.Status, | 	s *gtsmodel.Status, | ||||||
| 	requestingAccount *gtsmodel.Account, | 	requestingAccount *gtsmodel.Account, | ||||||
| ) (*apimodel.Status, error) { | ) (*apimodel.Status, error) { | ||||||
|  | 	// Try to populate status struct pointer fields. | ||||||
|  | 	// We can continue in many cases of partial failure, | ||||||
|  | 	// 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 { | ||||||
| 		// Ensure author account present + correct; |  | ||||||
| 		// can't really go further without this! |  | ||||||
| 		if s.Account == nil { | 		if s.Account == nil { | ||||||
| 			return nil, gtserror.Newf("error(s) populating status, cannot continue: %w", err) | 			err = gtserror.Newf("error(s) populating status, cannot continue (status.Account not set): %w", err) | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if 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, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		log.Errorf(ctx, "error(s) populating status, will continue: %v", err) | 		log.Errorf(ctx, "error(s) populating status, will continue: %v", err) | ||||||
|  | @ -794,12 +801,12 @@ func (c *Converter) statusToFrontend( | ||||||
| 	apiStatus := &apimodel.Status{ | 	apiStatus := &apimodel.Status{ | ||||||
| 		ID:                 s.ID, | 		ID:                 s.ID, | ||||||
| 		CreatedAt:          util.FormatISO8601(s.CreatedAt), | 		CreatedAt:          util.FormatISO8601(s.CreatedAt), | ||||||
| 		InReplyToID:        nil, | 		InReplyToID:        nil, // Set below. | ||||||
| 		InReplyToAccountID: nil, | 		InReplyToAccountID: nil, // Set below. | ||||||
| 		Sensitive:          *s.Sensitive, | 		Sensitive:          *s.Sensitive, | ||||||
| 		SpoilerText:        s.ContentWarning, | 		SpoilerText:        s.ContentWarning, | ||||||
| 		Visibility:         c.VisToAPIVis(ctx, s.Visibility), | 		Visibility:         c.VisToAPIVis(ctx, s.Visibility), | ||||||
| 		Language:           nil, | 		Language:           nil, // Set below. | ||||||
| 		URI:                s.URI, | 		URI:                s.URI, | ||||||
| 		URL:                s.URL, | 		URL:                s.URL, | ||||||
| 		RepliesCount:       repliesCount, | 		RepliesCount:       repliesCount, | ||||||
|  | @ -811,58 +818,49 @@ func (c *Converter) statusToFrontend( | ||||||
| 		Reblogged:          interacts.Reblogged, | 		Reblogged:          interacts.Reblogged, | ||||||
| 		Pinned:             interacts.Pinned, | 		Pinned:             interacts.Pinned, | ||||||
| 		Content:            s.Content, | 		Content:            s.Content, | ||||||
| 		Reblog:             nil, | 		Reblog:             nil, // Set below. | ||||||
| 		Application:        nil, | 		Application:        nil, // Set below. | ||||||
| 		Account:            apiAuthorAccount, | 		Account:            apiAuthorAccount, | ||||||
| 		MediaAttachments:   apiAttachments, | 		MediaAttachments:   apiAttachments, | ||||||
| 		Mentions:           apiMentions, | 		Mentions:           apiMentions, | ||||||
| 		Tags:               apiTags, | 		Tags:               apiTags, | ||||||
| 		Emojis:             apiEmojis, | 		Emojis:             apiEmojis, | ||||||
| 		Card:               nil, // TODO: implement cards | 		Card:               nil, // TODO: implement cards | ||||||
| 		Poll:               nil, // TODO: implement polls |  | ||||||
| 		Text:               s.Text, | 		Text:               s.Text, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Nullable fields. | 	// Nullable fields. | ||||||
| 
 |  | ||||||
| 	if s.InReplyToID != "" { | 	if s.InReplyToID != "" { | ||||||
| 		apiStatus.InReplyToID = func() *string { i := s.InReplyToID; return &i }() | 		apiStatus.InReplyToID = util.Ptr(s.InReplyToID) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if s.InReplyToAccountID != "" { | 	if s.InReplyToAccountID != "" { | ||||||
| 		apiStatus.InReplyToAccountID = func() *string { i := s.InReplyToAccountID; return &i }() | 		apiStatus.InReplyToAccountID = util.Ptr(s.InReplyToAccountID) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if s.Language != "" { | 	if s.Language != "" { | ||||||
| 		apiStatus.Language = func() *string { i := s.Language; return &i }() | 		apiStatus.Language = util.Ptr(s.Language) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if s.BoostOf != nil { | 	if s.BoostOf != nil { | ||||||
| 		apiBoostOf, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount) | 		reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, gtserror.Newf("error converting boosted status: %w", err) | 			return nil, gtserror.Newf("error converting boosted status: %w", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		apiStatus.Reblog = &apimodel.StatusReblogged{Status: apiBoostOf} | 		apiStatus.Reblog = &apimodel.StatusReblogged{reblog} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if appID := s.CreatedWithApplicationID; appID != "" { | 	if app := s.CreatedWithApplication; app != nil { | ||||||
| 		app := s.CreatedWithApplication | 		apiStatus.Application, err = c.AppToAPIAppPublic(ctx, app) | ||||||
| 		if app == nil { |  | ||||||
| 			app, err = c.state.DB.GetApplicationByID(ctx, appID) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 				return nil, gtserror.Newf("error getting application %s: %w", appID, err) | 			return nil, gtserror.Newf( | ||||||
|  | 				"error converting application %s: %w", | ||||||
|  | 				s.CreatedWithApplicationID, err, | ||||||
|  | 			) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		apiApp, err := c.AppToAPIAppPublic(ctx, app) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, gtserror.Newf("error converting application %s: %w", appID, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		apiStatus.Application = apiApp |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if s.Poll != nil { | 	if s.Poll != nil { | ||||||
| 		// Set originating | 		// Set originating | ||||||
| 		// status on the poll. | 		// status on the poll. | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ package web | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | @ -124,6 +125,13 @@ func (m *Module) threadGETHandler(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Don't render boosts/reblogs as top-level statuses. | ||||||
|  | 	if status.Reblog != nil { | ||||||
|  | 		err := errors.New("status is a boost wrapper / reblog") | ||||||
|  | 		apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Fill in the rest of the thread context. | 	// Fill in the rest of the thread context. | ||||||
| 	context, errWithCode := m.processor.Status().WebContextGet(ctx, targetStatusID) | 	context, errWithCode := m.processor.Status().WebContextGet(ctx, targetStatusID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue