diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go index 1dc9dcf1b..b2af5583f 100644 --- a/internal/db/bundb/timeline.go +++ b/internal/db/bundb/timeline.go @@ -50,6 +50,64 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI 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. NewSelect(). 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) } - // 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 // accounts with IDs in the slice. q = q.Where( diff --git a/internal/db/bundb/timeline_test.go b/internal/db/bundb/timeline_test.go index b944bd9b4..4874c2b35 100644 --- a/internal/db/bundb/timeline_test.go +++ b/internal/db/bundb/timeline_test.go @@ -158,6 +158,46 @@ func (suite *TimelineTestSuite) TestGetHomeTimeline() { 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() { var ( ctx = context.Background()