[bugfix] Update home timeline query to ignore exclusive list entries (#3289)

* [bugfix] Update home timeline query to ignore exclusive list entries

* a
This commit is contained in:
tobi 2024-09-11 12:55:25 +02:00 committed by GitHub
parent d842069985
commit 20fe430ef9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 27 deletions

View file

@ -50,6 +50,64 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
frontToBack = true frontToBack = true
) )
// As this is the home timeline, it should be
// populated by statuses from accounts followed
// by accountID, and posts from accountID itself.
//
// So, begin by seeing who accountID follows.
// It should be a little cheaper to do this in
// a separate query like this, rather than using
// a join, since followIDs are cached in memory.
follows, err := t.state.DB.GetAccountFollows(
gtscontext.SetBarebones(ctx),
accountID,
nil, // select all
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.Newf("db error getting follows for account %s: %w", accountID, err)
}
// To take account of exclusive lists, get all of
// this account's lists, so we can filter out follows
// that are in contained in exclusive lists.
lists, err := t.state.DB.GetListsForAccountID(ctx, accountID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.Newf("db error getting lists for account %s: %w", accountID, err)
}
// Index all follow IDs that fall in exclusive lists.
ignoreFollowIDs := make(map[string]struct{})
for _, list := range lists {
if !*list.Exclusive {
// Not exclusive,
// we don't care.
continue
}
// Exclusive list, index all its follow IDs.
for _, listEntry := range list.ListEntries {
ignoreFollowIDs[listEntry.FollowID] = struct{}{}
}
}
// Extract just the accountID from each follow,
// ignoring follows that are in exclusive lists.
targetAccountIDs := make([]string, 0, len(follows)+1)
for _, f := range follows {
_, ignore := ignoreFollowIDs[f.ID]
if !ignore {
targetAccountIDs = append(
targetAccountIDs,
f.TargetAccountID,
)
}
}
// Add accountID itself as a pseudo follow so that
// accountID can see its own posts in the timeline.
targetAccountIDs = append(targetAccountIDs, accountID)
// Now start building the database query.
q := t.db. q := t.db.
NewSelect(). NewSelect().
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
@ -89,33 +147,6 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
q = q.Where("? = ?", bun.Ident("status.local"), local) q = q.Where("? = ?", bun.Ident("status.local"), local)
} }
// As this is the home timeline, it should be
// populated by statuses from accounts followed
// by accountID, and posts from accountID itself.
//
// So, begin by seeing who accountID follows.
// It should be a little cheaper to do this in
// a separate query like this, rather than using
// a join, since followIDs are cached in memory.
follows, err := t.state.DB.GetAccountFollows(
gtscontext.SetBarebones(ctx),
accountID,
nil, // select all
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.Newf("db error getting follows for account %s: %w", accountID, err)
}
// Extract just the accountID from each follow.
targetAccountIDs := make([]string, len(follows)+1)
for i, f := range follows {
targetAccountIDs[i] = f.TargetAccountID
}
// Add accountID itself as a pseudo follow so that
// accountID can see its own posts in the timeline.
targetAccountIDs[len(targetAccountIDs)-1] = accountID
// Select only statuses authored by // Select only statuses authored by
// accounts with IDs in the slice. // accounts with IDs in the slice.
q = q.Where( q = q.Where(

View file

@ -158,6 +158,46 @@ func (suite *TimelineTestSuite) TestGetHomeTimeline() {
suite.checkStatuses(s, id.Highest, id.Lowest, 20) suite.checkStatuses(s, id.Highest, id.Lowest, 20)
} }
func (suite *TimelineTestSuite) TestGetHomeTimelineIgnoreExclusive() {
var (
ctx = context.Background()
viewingAccount = suite.testAccounts["local_account_1"]
)
// local_account_1_list_1 contains both admin_account
// and local_account_2. If we mark this list as exclusive,
// and remove the list entry for admin account, we should
// only get statuses from zork and turtle in the timeline.
list := new(gtsmodel.List)
*list = *suite.testLists["local_account_1_list_1"]
list.Exclusive = util.Ptr(true)
if err := suite.db.UpdateList(ctx, list, "exclusive"); err != nil {
suite.FailNow(err.Error())
}
// First try with list just set to exclusive.
// We should only get zork's own statuses.
s, err := suite.db.GetHomeTimeline(ctx, viewingAccount.ID, "", "", "", 20, false)
if err != nil {
suite.FailNow(err.Error())
}
suite.checkStatuses(s, id.Highest, id.Lowest, 8)
// Remove admin account from the exclusive list.
listEntryID := suite.testListEntries["local_account_1_list_1_entry_2"].ID
if err := suite.db.DeleteListEntry(ctx, listEntryID); err != nil {
suite.FailNow(err.Error())
}
// Zork should only see their own
// statuses and admin's statuses now.
s, err = suite.db.GetHomeTimeline(ctx, viewingAccount.ID, "", "", "", 20, false)
if err != nil {
suite.FailNow(err.Error())
}
suite.checkStatuses(s, id.Highest, id.Lowest, 12)
}
func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() { func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
var ( var (
ctx = context.Background() ctx = context.Background()