summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-12-01 15:27:15 +0100
committerLibravatar GitHub <noreply@github.com>2023-12-01 15:27:15 +0100
commit0e2c34219112db3a6b7801530a946fd5b1bbb111 (patch)
tree6a5557373dbfc9edc80de941b13e870a8af32881 /internal/processing
parent[bugfix] in fedi API CreateStatus(), handle case of data-race and return earl... (diff)
downloadgotosocial-0e2c34219112db3a6b7801530a946fd5b1bbb111.tar.xz
[bugfix/chore] `Announce` reliability updates (#2405)v0.13.0-rc1
* [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
Diffstat (limited to 'internal/processing')
-rw-r--r--internal/processing/common/status.go19
-rw-r--r--internal/processing/fedi/status.go10
-rw-r--r--internal/processing/status/boost.go161
-rw-r--r--internal/processing/status/fave.go32
-rw-r--r--internal/processing/workers/fromfediapi.go38
-rw-r--r--internal/processing/workers/fromfediapi_test.go4
6 files changed, 165 insertions, 99 deletions
diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go
index 233c1c867..0a1f495fb 100644
--- a/internal/processing/common/status.go
+++ b/internal/processing/common/status.go
@@ -119,6 +119,25 @@ func (p *Processor) GetVisibleTargetStatus(
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.
func (p *Processor) GetAPIStatus(
ctx context.Context,
diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go
index 8082c79bc..1c1af9cb4 100644
--- a/internal/processing/fedi/status.go
+++ b/internal/processing/fedi/status.go
@@ -51,6 +51,11 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
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)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
@@ -106,6 +111,11 @@ func (p *Processor) StatusRepliesGet(
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.
onlyOtherAccStr := "only_other_accounts=" + strconv.FormatBool(onlyOtherAccounts)
collectionID, err := url.Parse(status.URI + "/replies?" + onlyOtherAccStr)
diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go
index 76a0a75bc..2062fb802 100644
--- a/internal/processing/status/boost.go
+++ b/internal/processing/status/boost.go
@@ -30,106 +30,125 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
-// BoostCreate processes the boost/reblog of a given status, returning the newly-created boost if all is well.
-func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
+// BoostCreate processes the boost/reblog of target
+// status, returning the newly-created boost.
+func (p *Processor) BoostCreate(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ application *gtsmodel.Application,
+ targetID string,
+) (*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
+ }
+
+ target, errWithCode = p.c.UnwrapIfBoost(
+ ctx,
+ requester,
+ target,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Ensure valid boost target.
+ boostable, err := p.filter.StatusBoostable(ctx,
+ requester,
+ target,
+ )
if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- if targetStatus.Account == nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
- }
-
- // if targetStatusID refers to a boost, then we should redirect
- // the target to being the status that was boosted; if we don't
- // do this, then we end up in weird situations where people
- // boost boosts, and it looks absolutely bizarre in the UI
- if targetStatus.BoostOfID != "" {
- if targetStatus.BoostOf == nil {
- b, err := p.state.DB.GetStatusByID(ctx, targetStatus.BoostOfID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("couldn't fetch boosted status %s", targetStatus.BoostOfID))
- }
- targetStatus.BoostOf = b
- }
- targetStatus = targetStatus.BoostOf
+ err := gtserror.Newf("error seeing if status %s is boostable: %w", target.ID, err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- boostable, err := p.filter.StatusBoostable(ctx, requestingAccount, targetStatus)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is boostable: %s", targetStatus.ID, err))
- } else if !boostable {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not boostable"))
+ if !boostable {
+ err := gtserror.New("status is not boostable")
+ return nil, gtserror.NewErrorNotFound(err)
}
- // it's visible! it's boostable! so let's boost the FUCK out of it
- boostWrapperStatus, err := p.converter.StatusToBoost(ctx, targetStatus, requestingAccount)
+ // Status is visible and boostable.
+ boost, err := p.converter.StatusToBoost(ctx,
+ target,
+ requester,
+ application.ID,
+ )
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- boostWrapperStatus.CreatedWithApplicationID = application.ID
- boostWrapperStatus.BoostOfAccount = targetStatus.Account
-
- // put the boost in the database
- if err := p.state.DB.PutStatus(ctx, boostWrapperStatus); err != nil {
+ // Store the new boost.
+ if err := p.state.DB.PutStatus(ctx, boost); err != nil {
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{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
- GTSModel: boostWrapperStatus,
- OriginAccount: requestingAccount,
- TargetAccount: targetStatus.Account,
+ GTSModel: boost,
+ OriginAccount: requester,
+ 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.
-func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- if targetStatus.Account == nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
- }
-
- visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- // Check whether the requesting account has boosted the given status ID.
- boost, err := p.state.DB.GetStatusBoost(ctx, targetStatusID, requestingAccount.ID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error checking status boost %s: %w", targetStatusID, err))
+// BoostRemove processes the unboost/unreblog of
+// target status, returning the target status.
+func (p *Processor) BoostRemove(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ application *gtsmodel.Application,
+ targetID string,
+) (*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
+ }
+
+ target, errWithCode = p.c.UnwrapIfBoost(
+ ctx,
+ requester,
+ target,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Check whether requester has actually
+ // boosted target, by trying to get the boost.
+ boost, err := p.state.DB.GetStatusBoost(ctx,
+ 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 {
- // pin some stuff onto the boost while we have it out of the db
- boost.Account = requestingAccount
- boost.BoostOf = targetStatus
- boost.BoostOfAccount = targetStatus.Account
- boost.BoostOf.Account = targetStatus.Account
-
- // send it back to the processor for async processing
+ // Status was boosted. Process unboost side effects asynchronously.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityUndo,
GTSModel: boost,
- OriginAccount: requestingAccount,
- TargetAccount: targetStatus.Account,
+ OriginAccount: requester,
+ 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.
diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go
index a16fb6620..dbeba7fe9 100644
--- a/internal/processing/status/fave.go
+++ b/internal/processing/status/fave.go
@@ -33,24 +33,46 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
-func (p *Processor) getFaveableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) {
- targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
+func (p *Processor) getFaveableStatus(
+ 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 {
+ return nil, nil, errWithCode
+ }
+
+ target, errWithCode = p.c.UnwrapIfBoost(
+ ctx,
+ requester,
+ target,
+ )
if errWithCode != nil {
return nil, nil, errWithCode
}
- if !*targetStatus.Likeable {
+ if !*target.Likeable {
err := errors.New("status is not faveable")
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) {
err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", 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).
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 4c5c85666..d04e4ab8d 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -26,7 +26,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"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 {
- status, ok := fMsg.GTSModel.(*gtsmodel.Status)
+ boost, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
}
// 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.
- if err := p.federate.DereferenceAnnounce(ctx,
- status,
+ var err error
+ boost, err = p.federate.EnrichAnnounce(
+ ctx,
+ boost,
fMsg.ReceivingAccount.Username,
- ); err != nil {
- 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
+ if gtserror.IsUnretrievable(err) {
+ // Boosted status domain blocked, nothing to do.
+ log.Debugf(ctx, "skipping announce: %v", err)
+ return nil
+ }
- // Store the boost wrapper status.
- if err := p.state.DB.PutStatus(ctx, status); err != nil {
- return gtserror.Newf("db error inserting status: %w", err)
+ // Actual error.
+ return gtserror.Newf("error dereferencing announce: %w", err)
}
// 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)
}
- 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)
}
// Interaction counts changed on the original status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+ p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
return nil
}
diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go
index 952c008cc..799eaf2dc 100644
--- a/internal/processing/workers/fromfediapi_test.go
+++ b/internal/processing/workers/fromfediapi_test.go
@@ -45,9 +45,7 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
boostingAccount := suite.testAccounts["remote_account_1"]
announceStatus := &gtsmodel.Status{}
announceStatus.URI = "https://example.org/some-announce-uri"
- announceStatus.BoostOf = &gtsmodel.Status{
- URI: boostedStatus.URI,
- }
+ announceStatus.BoostOfURI = boostedStatus.URI
announceStatus.CreatedAt = time.Now()
announceStatus.UpdatedAt = time.Now()
announceStatus.AccountID = boostingAccount.ID