mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 19:52:25 -05:00 
			
		
		
		
	[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:
		
					parent
					
						
							
								d842069985
							
						
					
				
			
			
				commit
				
					
						20fe430ef9
					
				
			
		
					 2 changed files with 98 additions and 27 deletions
				
			
		|  | @ -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( | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue