[bugfix] unwrap boosts when checking in-reply-to status (#2702)

* add stronger checks on status being replied to

* update error code test is expecting
This commit is contained in:
kim 2024-02-29 14:20:57 +00:00 committed by GitHub
parent c2a691fd83
commit fcecd0c952
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 41 deletions

View file

@ -284,13 +284,13 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
// check response // check response
suite.EqualValues(http.StatusBadRequest, recorder.Code) suite.EqualValues(http.StatusNotFound, recorder.Code)
result := recorder.Result() result := recorder.Result()
defer result.Body.Close() defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) b, err := ioutil.ReadAll(result.Body)
suite.NoError(err) suite.NoError(err)
suite.Equal(`{"error":"Bad Request: cannot reply to status that does not exist"}`, string(b)) suite.Equal(`{"error":"Not Found: target status not found"}`, string(b))
} }
// Post a reply to the status of a local user that allows replies. // Post a reply to the status of a local user that allows replies.

View file

@ -30,6 +30,7 @@
"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"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/typeutils"
@ -40,12 +41,20 @@
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK. // Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
// //
// Precondition: the form's fields should have already been validated and normalized by the caller. // Precondition: the form's fields should have already been validated and normalized by the caller.
func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) { func (p *Processor) Create(
ctx context.Context,
requester *gtsmodel.Account,
application *gtsmodel.Application,
form *apimodel.AdvancedStatusCreateForm,
) (
*apimodel.Status,
gtserror.WithCode,
) {
// Generate new ID for status. // Generate new ID for status.
statusID := id.NewULID() statusID := id.NewULID()
// Generate necessary URIs for username, to build status URIs. // Generate necessary URIs for username, to build status URIs.
accountURIs := uris.GenerateURIsForAccount(requestingAccount.Username) accountURIs := uris.GenerateURIsForAccount(requester.Username)
// Get current time. // Get current time.
now := time.Now() now := time.Now()
@ -57,9 +66,9 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
Local: util.Ptr(true), Local: util.Ptr(true),
Account: requestingAccount, Account: requester,
AccountID: requestingAccount.ID, AccountID: requester.ID,
AccountURI: requestingAccount.URI, AccountURI: requester.URI,
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
Sensitive: &form.Sensitive, Sensitive: &form.Sensitive,
CreatedWithApplicationID: application.ID, CreatedWithApplicationID: application.ID,
@ -86,7 +95,12 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
status.PollID = status.Poll.ID status.PollID = status.Poll.ID
} }
if errWithCode := p.processReplyToID(ctx, form, requestingAccount.ID, status); errWithCode != nil { // Check + attach in-reply-to status.
if errWithCode := p.processInReplyTo(ctx,
requester,
status,
form.InReplyToID,
); errWithCode != nil {
return nil, errWithCode return nil, errWithCode
} }
@ -94,15 +108,15 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
return nil, errWithCode return nil, errWithCode
} }
if errWithCode := p.processMediaIDs(ctx, form, requestingAccount.ID, status); errWithCode != nil { if errWithCode := p.processMediaIDs(ctx, form, requester.ID, status); errWithCode != nil {
return nil, errWithCode return nil, errWithCode
} }
if err := processVisibility(form, requestingAccount.Privacy, status); err != nil { if err := processVisibility(form, requester.Privacy, status); err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
if err := processLanguage(form, requestingAccount.Language, status); err != nil { if err := processLanguage(form, requester.Language, status); err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
@ -128,58 +142,49 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
APObjectType: ap.ObjectNote, APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate, APActivityType: ap.ActivityCreate,
GTSModel: status, GTSModel: status,
OriginAccount: requestingAccount, OriginAccount: requester,
}) })
if status.Poll != nil { if status.Poll != nil {
// Now that the status is inserted, and side effects queued, // Now that the status is inserted, and side effects queued,
// attempt to schedule an expiry handler for the status poll. // attempt to schedule an expiry handler for the status poll.
if err := p.polls.ScheduleExpiry(ctx, status.Poll); err != nil { if err := p.polls.ScheduleExpiry(ctx, status.Poll); err != nil {
err := gtserror.Newf("error scheduling poll expiry: %w", err) log.Errorf(ctx, "error scheduling poll expiry: %v", err)
return nil, gtserror.NewErrorInternalError(err)
} }
} }
return p.c.GetAPIStatus(ctx, requestingAccount, status) return p.c.GetAPIStatus(ctx, requester, status)
} }
func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode { func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, inReplyToID string) gtserror.WithCode {
if form.InReplyToID == "" { if inReplyToID == "" {
return nil return nil
} }
// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted: // Fetch target in-reply-to status (checking visibility).
// inReplyTo, errWithCode := p.c.GetVisibleTargetStatus(ctx,
// 1. Does the replied status exist in the database? requester,
// 2. Is the replied status marked as replyable? inReplyToID,
// 3. Does a block exist between either the current account or the account that posted the status it's replying to? nil,
// )
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing. if errWithCode != nil {
return errWithCode
inReplyTo, err := p.state.DB.GetStatusByID(ctx, form.InReplyToID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("error fetching status %s from db: %w", form.InReplyToID, err)
return gtserror.NewErrorInternalError(err)
} }
if inReplyTo == nil { // If this is a boost, unwrap it to get source status.
const text = "cannot reply to status that does not exist" inReplyTo, errWithCode = p.c.UnwrapIfBoost(ctx,
return gtserror.NewErrorBadRequest(errors.New(text), text) requester,
inReplyTo,
)
if errWithCode != nil {
return errWithCode
} }
if !*inReplyTo.Replyable { if !*inReplyTo.Replyable {
text := fmt.Sprintf("status %s is marked as not replyable", form.InReplyToID) const text = "in-reply-to status marked as not replyable"
return gtserror.NewErrorForbidden(errors.New(text), text) return gtserror.NewErrorForbidden(errors.New(text), text)
} }
if blocked, err := p.state.DB.IsEitherBlocked(ctx, thisAccountID, inReplyTo.AccountID); err != nil {
err := gtserror.Newf("error checking block in db: %w", err)
return gtserror.NewErrorInternalError(err)
} else if blocked {
text := fmt.Sprintf("status %s is not replyable", form.InReplyToID)
return gtserror.NewErrorNotFound(errors.New(text), text)
}
// Set status fields from inReplyTo. // Set status fields from inReplyTo.
status.InReplyToID = inReplyTo.ID status.InReplyToID = inReplyTo.ID
status.InReplyTo = inReplyTo status.InReplyTo = inReplyTo