summaryrefslogtreecommitdiff
path: root/internal/processing/workers/surfacetimeline.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/workers/surfacetimeline.go')
-rw-r--r--internal/processing/workers/surfacetimeline.go169
1 files changed, 123 insertions, 46 deletions
diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go
index c0987effd..81544d928 100644
--- a/internal/processing/workers/surfacetimeline.go
+++ b/internal/processing/workers/surfacetimeline.go
@@ -34,7 +34,7 @@ import (
)
// timelineAndNotifyStatus inserts the given status into the HOME
-// and LIST timelines of accounts that follow the status author,
+// and/or LIST timelines of accounts that follow the status author,
// as well as the HOME timelines of accounts that follow tags used by the status.
//
// It will also handle notifications for any mentions attached to
@@ -100,6 +100,7 @@ func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
// new status, if the status is eligible for notification.
//
// Returns a list of accounts which had this status inserted into their home timelines.
+// This will be used to prevent duplicate inserts when handling followed tags.
func (s *Surface) timelineAndNotifyStatusForFollowers(
ctx context.Context,
status *gtsmodel.Status,
@@ -118,8 +119,13 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
// it's a reblog, whether follower account wants to see reblogs.
//
// If it's not timelineable, we can just stop early, since lists
- // are prettymuch subsets of the home timeline, so if it shouldn't
+ // are pretty much subsets of the home timeline, so if it shouldn't
// appear there, it shouldn't appear in lists either.
+ //
+ // Exclusive lists don't change this:
+ // if something is hometimelineable according to this filter,
+ // it's also eligible to appear in exclusive lists,
+ // even if it ultimately doesn't appear on the home timeline.
timelineable, err := s.VisFilter.StatusHomeTimelineable(
ctx, follow.Account, status,
)
@@ -141,7 +147,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
// Add status to any relevant lists
// for this follow, if applicable.
- s.listTimelineStatusForFollow(
+ exclusive, listTimelined := s.listTimelineStatusForFollow(
ctx,
status,
follow,
@@ -152,27 +158,32 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
// Add status to home timeline for owner
// of this follow, if applicable.
- homeTimelined, err := s.timelineStatus(
- ctx,
- s.State.Timelines.Home.IngestOne,
- follow.AccountID, // home timelines are keyed by account ID
- follow.Account,
- status,
- stream.TimelineHome,
- filters,
- mutes,
- )
- if err != nil {
- errs.Appendf("error home timelining status: %w", err)
- continue
+ homeTimelined := false
+ if !exclusive {
+ homeTimelined, err = s.timelineStatus(
+ ctx,
+ s.State.Timelines.Home.IngestOne,
+ follow.AccountID, // home timelines are keyed by account ID
+ follow.Account,
+ status,
+ stream.TimelineHome,
+ filters,
+ mutes,
+ )
+ if err != nil {
+ errs.Appendf("error home timelining status: %w", err)
+ continue
+ }
+ if homeTimelined {
+ homeTimelinedAccountIDs = append(homeTimelinedAccountIDs, follow.AccountID)
+ }
}
- if !homeTimelined {
- // If status wasn't added to home
- // timeline, we shouldn't notify it.
+ if !(homeTimelined || listTimelined) {
+ // If status wasn't added to home or list
+ // timelines, we shouldn't notify it.
continue
}
- homeTimelinedAccountIDs = append(homeTimelinedAccountIDs, follow.AccountID)
if !*follow.Notify {
// This follower doesn't have notifs
@@ -188,7 +199,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
// If we reach here, we know:
//
// - This status is hometimelineable.
- // - This status was added to the home timeline for this follower.
+ // - This status was added to the home timeline and/or list timelines for this follower.
// - This follower wants to be notified when this account posts.
// - This is a top-level post (not a reply or boost).
//
@@ -208,6 +219,10 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
// listTimelineStatusForFollow puts the given status
// in any eligible lists owned by the given follower.
+//
+// It returns whether the status was added to any lists,
+// and whether the status author is on any exclusive lists
+// (in which case the status shouldn't be added to the home timeline).
func (s *Surface) listTimelineStatusForFollow(
ctx context.Context,
status *gtsmodel.Status,
@@ -215,7 +230,7 @@ func (s *Surface) listTimelineStatusForFollow(
errs *gtserror.MultiError,
filters []*gtsmodel.Filter,
mutes *usermute.CompiledUserMuteList,
-) {
+) (bool, bool) {
// To put this status in appropriate list timelines,
// we need to get each listEntry that pertains to
// this follow. Then, we want to iterate through all
@@ -223,18 +238,19 @@ func (s *Surface) listTimelineStatusForFollow(
// that the entry belongs to if it meets criteria for
// inclusion in the list.
- // Get every list entry that targets this follow's ID.
- listEntries, err := s.State.DB.GetListEntriesForFollowID(
- // We only need the list IDs.
- gtscontext.SetBarebones(ctx),
- follow.ID,
- )
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- errs.Appendf("error getting list entries: %w", err)
- return
+ listEntries, err := s.getListEntries(ctx, follow)
+ if err != nil {
+ errs.Append(err)
+ return false, false
+ }
+ exclusive, err := s.isAnyListExclusive(ctx, listEntries)
+ if err != nil {
+ errs.Append(err)
+ return false, false
}
// Check eligibility for each list entry (if any).
+ listTimelined := false
for _, listEntry := range listEntries {
eligible, err := s.listEligible(ctx, listEntry, status)
if err != nil {
@@ -250,7 +266,7 @@ func (s *Surface) listTimelineStatusForFollow(
// At this point we are certain this status
// should be included in the timeline of the
// list that this list entry belongs to.
- if _, err := s.timelineStatus(
+ timelined, err := s.timelineStatus(
ctx,
s.State.Timelines.List.IngestOne,
listEntry.ListID, // list timelines are keyed by list ID
@@ -259,11 +275,59 @@ func (s *Surface) listTimelineStatusForFollow(
stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
filters,
mutes,
- ); err != nil {
+ )
+ if err != nil {
errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err)
// implicit continue
}
+ listTimelined = listTimelined || timelined
+ }
+
+ return exclusive, listTimelined
+}
+
+// getListEntries returns list entries for a given follow.
+func (s *Surface) getListEntries(ctx context.Context, follow *gtsmodel.Follow) ([]*gtsmodel.ListEntry, error) {
+ // Get every list entry that targets this follow's ID.
+ listEntries, err := s.State.DB.GetListEntriesForFollowID(
+ // We only need the list IDs.
+ gtscontext.SetBarebones(ctx),
+ follow.ID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.Newf("DB error getting list entries: %v", err)
+ }
+ return listEntries, nil
+}
+
+// isAnyListExclusive determines whether any provided list entry corresponds to an exclusive list.
+func (s *Surface) isAnyListExclusive(ctx context.Context, listEntries []*gtsmodel.ListEntry) (bool, error) {
+ if len(listEntries) == 0 {
+ return false, nil
+ }
+
+ listIDs := make([]string, 0, len(listEntries))
+ for _, listEntry := range listEntries {
+ listIDs = append(listIDs, listEntry.ListID)
+ }
+ lists, err := s.State.DB.GetListsByIDs(
+ // We only need the list exclusive flags.
+ gtscontext.SetBarebones(ctx),
+ listIDs,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return false, gtserror.Newf("DB error getting lists for list entries: %v", err)
+ }
+
+ if len(lists) == 0 {
+ return false, nil
+ }
+ for _, list := range lists {
+ if *list.Exclusive {
+ return true, nil
+ }
}
+ return false, nil
}
// getFiltersAndMutes returns an account's filters and mutes.
@@ -643,8 +707,13 @@ func (s *Surface) timelineStatusUpdateForFollowers(
// it's a reblog, whether follower account wants to see reblogs.
//
// If it's not timelineable, we can just stop early, since lists
- // are prettymuch subsets of the home timeline, so if it shouldn't
+ // are pretty much subsets of the home timeline, so if it shouldn't
// appear there, it shouldn't appear in lists either.
+ //
+ // Exclusive lists don't change this:
+ // if something is hometimelineable according to this filter,
+ // it's also eligible to appear in exclusive lists,
+ // even if it ultimately doesn't appear on the home timeline.
timelineable, err := s.VisFilter.StatusHomeTimelineable(
ctx, follow.Account, status,
)
@@ -666,7 +735,7 @@ func (s *Surface) timelineStatusUpdateForFollowers(
// Add status to any relevant lists
// for this follow, if applicable.
- s.listTimelineStatusUpdateForFollow(
+ exclusive := s.listTimelineStatusUpdateForFollow(
ctx,
status,
follow,
@@ -675,6 +744,10 @@ func (s *Surface) timelineStatusUpdateForFollowers(
mutes,
)
+ if exclusive {
+ continue
+ }
+
// Add status to home timeline for owner
// of this follow, if applicable.
homeTimelined, err := s.timelineStreamStatusUpdate(
@@ -689,7 +762,6 @@ func (s *Surface) timelineStatusUpdateForFollowers(
errs.Appendf("error home timelining status: %w", err)
continue
}
-
if homeTimelined {
homeTimelinedAccountIDs = append(homeTimelinedAccountIDs, follow.AccountID)
}
@@ -700,6 +772,9 @@ func (s *Surface) timelineStatusUpdateForFollowers(
// listTimelineStatusUpdateForFollow pushes edits of the given status
// into any eligible lists streams opened by the given follower.
+//
+// It returns whether the status author is on any exclusive lists
+// (in which case the status shouldn't be added to the home timeline).
func (s *Surface) listTimelineStatusUpdateForFollow(
ctx context.Context,
status *gtsmodel.Status,
@@ -707,7 +782,7 @@ func (s *Surface) listTimelineStatusUpdateForFollow(
errs *gtserror.MultiError,
filters []*gtsmodel.Filter,
mutes *usermute.CompiledUserMuteList,
-) {
+) bool {
// To put this status in appropriate list timelines,
// we need to get each listEntry that pertains to
// this follow. Then, we want to iterate through all
@@ -715,15 +790,15 @@ func (s *Surface) listTimelineStatusUpdateForFollow(
// that the entry belongs to if it meets criteria for
// inclusion in the list.
- // Get every list entry that targets this follow's ID.
- listEntries, err := s.State.DB.GetListEntriesForFollowID(
- // We only need the list IDs.
- gtscontext.SetBarebones(ctx),
- follow.ID,
- )
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- errs.Appendf("error getting list entries: %w", err)
- return
+ listEntries, err := s.getListEntries(ctx, follow)
+ if err != nil {
+ errs.Append(err)
+ return false
+ }
+ exclusive, err := s.isAnyListExclusive(ctx, listEntries)
+ if err != nil {
+ errs.Append(err)
+ return false
}
// Check eligibility for each list entry (if any).
@@ -754,6 +829,8 @@ func (s *Surface) listTimelineStatusUpdateForFollow(
// implicit continue
}
}
+
+ return exclusive
}
// timelineStatusUpdate streams the edited status to the user using the