Skip to content

Commit

Permalink
[bugfix] Invalidate timeline entries for status when stats change (#1879
Browse files Browse the repository at this point in the history
)
  • Loading branch information
tsmethurst committed Jun 11, 2023
1 parent 84e1c7a commit 5e2897e
Show file tree
Hide file tree
Showing 12 changed files with 529 additions and 128 deletions.
43 changes: 43 additions & 0 deletions internal/db/bundb/statusfave.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
Expand Down Expand Up @@ -145,6 +146,48 @@ func (s *statusFaveDB) GetStatusFavesForStatus(ctx context.Context, statusID str
return faves, nil
}

func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error {
var (
err error
errs = make(gtserror.MultiError, 0, 3)
)

if statusFave.Account == nil {
// StatusFave author is not set, fetch from database.
statusFave.Account, err = s.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
statusFave.AccountID,
)
if err != nil {
errs.Append(fmt.Errorf("error populating status fave author: %w", err))
}
}

if statusFave.TargetAccount == nil {
// StatusFave target account is not set, fetch from database.
statusFave.TargetAccount, err = s.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
statusFave.TargetAccountID,
)
if err != nil {
errs.Append(fmt.Errorf("error populating status fave target account: %w", err))
}
}

if statusFave.Status == nil {
// StatusFave status is not set, fetch from database.
statusFave.Status, err = s.state.DB.GetStatusByID(
gtscontext.SetBarebones(ctx),
statusFave.StatusID,
)
if err != nil {
errs.Append(fmt.Errorf("error populating status fave status: %w", err))
}
}

return errs.Combine()
}

func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusFave) db.Error {
return s.state.Caches.GTS.StatusFave().Store(fave, func() error {
_, err := s.conn.
Expand Down
3 changes: 3 additions & 0 deletions internal/db/statusfave.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type StatusFave interface {
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
GetStatusFavesForStatus(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, Error)

// PopulateStatusFave ensures that all sub-models of a fave are populated (account, status, etc).
PopulateStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error

// PutStatusFave inserts the given statusFave into the database.
PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) Error

Expand Down
124 changes: 89 additions & 35 deletions internal/processing/fromclientapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
Expand Down Expand Up @@ -157,14 +158,24 @@ func (p *Processor) processCreateAccountFromClientAPI(ctx context.Context, clien
func (p *Processor) processCreateStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("note was not parseable as *gtsmodel.Status")
return gtserror.New("status was not parseable as *gtsmodel.Status")
}

if err := p.timelineAndNotifyStatus(ctx, status); err != nil {
return err
return gtserror.Newf("error timelining status: %w", err)
}

if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.invalidateStatusFromTimelines(ctx, status.InReplyToID)
}

if err := p.federateStatus(ctx, status); err != nil {
return gtserror.Newf("error federating status: %w", err)
}

return p.federateStatus(ctx, status)
return nil
}

func (p *Processor) processCreateFollowRequestFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
Expand All @@ -181,33 +192,50 @@ func (p *Processor) processCreateFollowRequestFromClientAPI(ctx context.Context,
}

func (p *Processor) processCreateFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
statusFave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return errors.New("fave was not parseable as *gtsmodel.StatusFave")
return gtserror.New("statusFave was not parseable as *gtsmodel.StatusFave")
}

if err := p.notifyFave(ctx, fave); err != nil {
return err
if err := p.notifyFave(ctx, statusFave); err != nil {
return gtserror.Newf("error notifying status fave: %w", err)
}

// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
p.invalidateStatusFromTimelines(ctx, statusFave.StatusID)

if err := p.federateFave(ctx, statusFave, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
return gtserror.Newf("error federating status fave: %w", err)
}

return p.federateFave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
return nil
}

func (p *Processor) processCreateAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("boost was not parseable as *gtsmodel.Status")
}

if err := p.timelineAndNotifyStatus(ctx, boostWrapperStatus); err != nil {
return err
// Timeline and notify.
if err := p.timelineAndNotifyStatus(ctx, status); err != nil {
return gtserror.Newf("error timelining boost: %w", err)
}

if err := p.notifyAnnounce(ctx, boostWrapperStatus); err != nil {
return err
if err := p.notifyAnnounce(ctx, status); err != nil {
return gtserror.Newf("error notifying boost: %w", err)
}

return p.federateAnnounce(ctx, boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount)
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
p.invalidateStatusFromTimelines(ctx, status.BoostOfID)

if err := p.federateAnnounce(ctx, status, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
return gtserror.Newf("error federating boost: %w", err)
}

return nil
}

func (p *Processor) processCreateBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
Expand Down Expand Up @@ -293,50 +321,76 @@ func (p *Processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg
}

func (p *Processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
statusFave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return errors.New("undo was not parseable as *gtsmodel.StatusFave")
return gtserror.New("statusFave was not parseable as *gtsmodel.StatusFave")
}
return p.federateUnfave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount)

// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
p.invalidateStatusFromTimelines(ctx, statusFave.StatusID)

if err := p.federateUnfave(ctx, statusFave, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
return gtserror.Newf("error federating status unfave: %w", err)
}

return nil
}

func (p *Processor) processUndoAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
boost, ok := clientMsg.GTSModel.(*gtsmodel.Status)
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("undo was not parseable as *gtsmodel.Status")
return errors.New("boost was not parseable as *gtsmodel.Status")
}

if err := p.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
return err
if err := p.state.DB.DeleteStatusByID(ctx, status.ID); err != nil {
return gtserror.Newf("db error deleting boost: %w", err)
}

if err := p.deleteStatusFromTimelines(ctx, boost); err != nil {
return err
if err := p.deleteStatusFromTimelines(ctx, status.ID); err != nil {
return gtserror.Newf("error removing boost from timelines: %w", err)
}

return p.federateUnannounce(ctx, boost, clientMsg.OriginAccount, clientMsg.TargetAccount)
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
p.invalidateStatusFromTimelines(ctx, status.BoostOfID)

if err := p.federateUnannounce(ctx, status, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
return gtserror.Newf("error federating status unboost: %w", err)
}

return nil
}

func (p *Processor) processDeleteStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status)
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return errors.New("note was not parseable as *gtsmodel.Status")
return gtserror.New("status was not parseable as *gtsmodel.Status")
}

if statusToDelete.Account == nil {
statusToDelete.Account = clientMsg.OriginAccount
if err := p.state.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("db error populating status: %w", err)
}

// don't delete attachments, just unattach them;
// since this request comes from the client API
// and the poster might want to use the attachments
// again in a new post
// Don't delete attachments, just unattach them: this
// request comes from the client API and the poster
// may want to use attachments again in a new post.
deleteAttachments := false
if err := p.wipeStatus(ctx, statusToDelete, deleteAttachments); err != nil {
return err
if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
return gtserror.Newf("error wiping status: %w", err)
}

return p.federateStatusDelete(ctx, statusToDelete)
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.invalidateStatusFromTimelines(ctx, status.InReplyToID)
}

if err := p.federateStatusDelete(ctx, status); err != nil {
return gtserror.Newf("error federating status delete: %w", err)
}

return nil
}

func (p *Processor) processDeleteAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
Expand Down
33 changes: 27 additions & 6 deletions internal/processing/fromcommon.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"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/stream"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
)
Expand Down Expand Up @@ -419,7 +420,7 @@ func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta
// delete all boosts for this status + remove them from timelines
if boosts, err := p.state.DB.GetStatusReblogs(ctx, statusToDelete); err == nil {
for _, b := range boosts {
if err := p.deleteStatusFromTimelines(ctx, b); err != nil {
if err := p.deleteStatusFromTimelines(ctx, b.ID); err != nil {
return err
}
if err := p.state.DB.DeleteStatusByID(ctx, b.ID); err != nil {
Expand All @@ -429,7 +430,7 @@ func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta
}

// delete this status from any and all timelines
if err := p.deleteStatusFromTimelines(ctx, statusToDelete); err != nil {
if err := p.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
return err
}

Expand All @@ -439,16 +440,36 @@ func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta

// deleteStatusFromTimelines completely removes the given status from all timelines.
// It will also stream deletion of the status to all open streams.
func (p *Processor) deleteStatusFromTimelines(ctx context.Context, status *gtsmodel.Status) error {
if err := p.state.Timelines.Home.WipeItemFromAllTimelines(ctx, status.ID); err != nil {
func (p *Processor) deleteStatusFromTimelines(ctx context.Context, statusID string) error {
if err := p.state.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil {
return err
}

if err := p.state.Timelines.List.WipeItemFromAllTimelines(ctx, status.ID); err != nil {
if err := p.state.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil {
return err
}

return p.stream.Delete(status.ID)
return p.stream.Delete(statusID)
}

// invalidateStatusFromTimelines does cache invalidation on the given status by
// unpreparing it from all timelines, forcing it to be prepared again (with updated
// stats, boost counts, etc) next time it's fetched by the timeline owner. This goes
// both for the status itself, and for any boosts of the status.
func (p *Processor) invalidateStatusFromTimelines(ctx context.Context, statusID string) {
if err := p.state.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
log.
WithContext(ctx).
WithField("statusID", statusID).
Errorf("error unpreparing status from home timelines: %v", err)
}

if err := p.state.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
log.
WithContext(ctx).
WithField("statusID", statusID).
Errorf("error unpreparing status from list timelines: %v", err)
}
}

/*
Expand Down
Loading

0 comments on commit 5e2897e

Please sign in to comment.