mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
@ -30,12 +30,19 @@ import (
|
||||
|
||||
// Create creates one a new list for the given account, using the provided parameters.
|
||||
// These params should have already been validated by the time they reach this function.
|
||||
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, title string, repliesPolicy gtsmodel.RepliesPolicy) (*apimodel.List, gtserror.WithCode) {
|
||||
func (p *Processor) Create(
|
||||
ctx context.Context,
|
||||
account *gtsmodel.Account,
|
||||
title string,
|
||||
repliesPolicy gtsmodel.RepliesPolicy,
|
||||
exclusive bool,
|
||||
) (*apimodel.List, gtserror.WithCode) {
|
||||
list := >smodel.List{
|
||||
ID: id.NewULID(),
|
||||
Title: title,
|
||||
AccountID: account.ID,
|
||||
RepliesPolicy: repliesPolicy,
|
||||
Exclusive: &exclusive,
|
||||
}
|
||||
|
||||
if err := p.state.DB.PutList(ctx, list); err != nil {
|
||||
|
@ -47,7 +47,7 @@ func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, id strin
|
||||
return p.apiList(ctx, list)
|
||||
}
|
||||
|
||||
// GetMultiple returns multiple lists created by the given account, sorted by list ID DESC (newest first).
|
||||
// GetAll returns multiple lists created by the given account, sorted by list ID DESC (newest first).
|
||||
func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.List, gtserror.WithCode) {
|
||||
lists, err := p.state.DB.GetListsForAccountID(
|
||||
// Use barebones ctx; no embedded
|
||||
|
@ -36,6 +36,7 @@ func (p *Processor) Update(
|
||||
id string,
|
||||
title *string,
|
||||
repliesPolicy *gtsmodel.RepliesPolicy,
|
||||
exclusive *bool,
|
||||
) (*apimodel.List, gtserror.WithCode) {
|
||||
list, errWithCode := p.getList(
|
||||
// Use barebones ctx; no embedded
|
||||
@ -49,7 +50,7 @@ func (p *Processor) Update(
|
||||
}
|
||||
|
||||
// Only update columns we're told to update.
|
||||
columns := make([]string, 0, 2)
|
||||
columns := make([]string, 0, 3)
|
||||
|
||||
if title != nil {
|
||||
list.Title = *title
|
||||
@ -61,6 +62,11 @@ func (p *Processor) Update(
|
||||
columns = append(columns, "replies_policy")
|
||||
}
|
||||
|
||||
if exclusive != nil {
|
||||
list.Exclusive = exclusive
|
||||
columns = append(columns, "exclusive")
|
||||
}
|
||||
|
||||
if err := p.state.DB.UpdateList(ctx, list, columns...); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("you already have a list with this title")
|
||||
|
@ -1527,6 +1527,317 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
|
||||
)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who follows the author and has them on an exclusive list
|
||||
// should end up in the following user's timeline for that list, but not their home timeline.
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiveList() {
|
||||
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||
defer testrig.TearDownTestStructs(testStructs)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
postingAccount = suite.testAccounts["local_account_2"]
|
||||
receivingAccount = suite.testAccounts["local_account_1"]
|
||||
testList = suite.testLists["local_account_1_list_1"]
|
||||
streams = suite.openStreams(ctx,
|
||||
testStructs.Processor,
|
||||
receivingAccount,
|
||||
[]string{testList.ID},
|
||||
)
|
||||
homeStream = streams[stream.TimelineHome]
|
||||
listStream = streams[stream.TimelineList+":"+testList.ID]
|
||||
|
||||
// postingAccount posts a new public status not mentioning anyone.
|
||||
status = suite.newStatus(
|
||||
ctx,
|
||||
testStructs.State,
|
||||
postingAccount,
|
||||
gtsmodel.VisibilityPublic,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// Setup: make the list exclusive.
|
||||
// We modify the existing list rather than create a new one, so that there's only one list in play for this test.
|
||||
list := new(gtsmodel.List)
|
||||
*list = *testList
|
||||
list.Exclusive = util.Ptr(true)
|
||||
if err := testStructs.State.DB.UpdateList(ctx, list); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Process the new status.
|
||||
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
|
||||
ctx,
|
||||
&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: status,
|
||||
Origin: postingAccount,
|
||||
},
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Check status in list stream.
|
||||
suite.checkStreamed(
|
||||
listStream,
|
||||
true,
|
||||
"",
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check status not in home stream.
|
||||
suite.checkStreamed(
|
||||
homeStream,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who follows the author and has them on an exclusive list
|
||||
// should end up in the following user's timeline for that list, but not their home timeline.
|
||||
// This should happen regardless of whether the author is on any of the following user's *non*-exclusive lists.
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiveAndNonExclusiveLists() {
|
||||
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||
defer testrig.TearDownTestStructs(testStructs)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
postingAccount = suite.testAccounts["local_account_2"]
|
||||
receivingAccount = suite.testAccounts["local_account_1"]
|
||||
testInclusiveList = suite.testLists["local_account_1_list_1"]
|
||||
testExclusiveList = >smodel.List{
|
||||
ID: id.NewULID(),
|
||||
Title: "Cool Ass Posters From This Instance (exclusive)",
|
||||
AccountID: receivingAccount.ID,
|
||||
RepliesPolicy: gtsmodel.RepliesPolicyFollowed,
|
||||
Exclusive: util.Ptr(true),
|
||||
}
|
||||
testFollow = suite.testFollows["local_account_1_local_account_2"]
|
||||
testExclusiveListEntries = []*gtsmodel.ListEntry{
|
||||
{
|
||||
ID: id.NewULID(),
|
||||
ListID: testExclusiveList.ID,
|
||||
FollowID: testFollow.ID,
|
||||
},
|
||||
}
|
||||
streams = suite.openStreams(ctx,
|
||||
testStructs.Processor,
|
||||
receivingAccount,
|
||||
[]string{
|
||||
testInclusiveList.ID,
|
||||
testExclusiveList.ID,
|
||||
},
|
||||
)
|
||||
homeStream = streams[stream.TimelineHome]
|
||||
inclusiveListStream = streams[stream.TimelineList+":"+testInclusiveList.ID]
|
||||
exclusiveListStream = streams[stream.TimelineList+":"+testExclusiveList.ID]
|
||||
|
||||
// postingAccount posts a new public status not mentioning anyone.
|
||||
status = suite.newStatus(
|
||||
ctx,
|
||||
testStructs.State,
|
||||
postingAccount,
|
||||
gtsmodel.VisibilityPublic,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// Precondition: the pre-existing inclusive list should actually be inclusive.
|
||||
// This should be the case if we reset the DB correctly between tests in this file.
|
||||
{
|
||||
list, err := testStructs.State.DB.GetListByID(ctx, testInclusiveList.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if *list.Exclusive {
|
||||
suite.FailNowf(
|
||||
"test precondition failed: list %s should be inclusive, but isn't",
|
||||
testInclusiveList.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup: create the exclusive list and its list entry.
|
||||
if err := testStructs.State.DB.PutList(ctx, testExclusiveList); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if err := testStructs.State.DB.PutListEntries(ctx, testExclusiveListEntries); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Process the new status.
|
||||
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
|
||||
ctx,
|
||||
&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: status,
|
||||
Origin: postingAccount,
|
||||
},
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Check status in inclusive list stream.
|
||||
suite.checkStreamed(
|
||||
inclusiveListStream,
|
||||
true,
|
||||
"",
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check status in exclusive list stream.
|
||||
suite.checkStreamed(
|
||||
exclusiveListStream,
|
||||
true,
|
||||
"",
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check status not in home stream.
|
||||
suite.checkStreamed(
|
||||
homeStream,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who follows the author and has them on an exclusive list
|
||||
// should end up in the following user's timeline for that list, but not their home timeline.
|
||||
// When they have notifications on for that user, they should be notified.
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiveListAndNotificationsOn() {
|
||||
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||
defer testrig.TearDownTestStructs(testStructs)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
postingAccount = suite.testAccounts["local_account_2"]
|
||||
receivingAccount = suite.testAccounts["local_account_1"]
|
||||
testFollow = suite.testFollows["local_account_1_local_account_2"]
|
||||
testList = suite.testLists["local_account_1_list_1"]
|
||||
streams = suite.openStreams(ctx,
|
||||
testStructs.Processor,
|
||||
receivingAccount,
|
||||
[]string{testList.ID},
|
||||
)
|
||||
homeStream = streams[stream.TimelineHome]
|
||||
listStream = streams[stream.TimelineList+":"+testList.ID]
|
||||
notifStream = streams[stream.TimelineNotifications]
|
||||
|
||||
// postingAccount posts a new public status not mentioning anyone.
|
||||
status = suite.newStatus(
|
||||
ctx,
|
||||
testStructs.State,
|
||||
postingAccount,
|
||||
gtsmodel.VisibilityPublic,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// Setup: Update the follow from receiving account -> posting account so
|
||||
// that receiving account wants notifs when posting account posts.
|
||||
follow := new(gtsmodel.Follow)
|
||||
*follow = *testFollow
|
||||
follow.Notify = util.Ptr(true)
|
||||
if err := testStructs.State.DB.UpdateFollow(ctx, follow); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Setup: make the list exclusive.
|
||||
list := new(gtsmodel.List)
|
||||
*list = *testList
|
||||
list.Exclusive = util.Ptr(true)
|
||||
if err := testStructs.State.DB.UpdateList(ctx, list); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Process the new status.
|
||||
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
|
||||
ctx,
|
||||
&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: status,
|
||||
Origin: postingAccount,
|
||||
},
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Check status in list stream.
|
||||
suite.checkStreamed(
|
||||
listStream,
|
||||
true,
|
||||
"",
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Wait for a notification to appear for the status.
|
||||
var notif *gtsmodel.Notification
|
||||
if !testrig.WaitFor(func() bool {
|
||||
var err error
|
||||
notif, err = testStructs.State.DB.GetNotification(
|
||||
ctx,
|
||||
gtsmodel.NotificationStatus,
|
||||
receivingAccount.ID,
|
||||
postingAccount.ID,
|
||||
status.ID,
|
||||
)
|
||||
return err == nil
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for new status notification")
|
||||
}
|
||||
|
||||
apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil, nil)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
notifJSON, err := json.Marshal(apiNotif)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Check message in notification stream.
|
||||
suite.checkStreamed(
|
||||
notifStream,
|
||||
true,
|
||||
string(notifJSON),
|
||||
stream.EventTypeNotification,
|
||||
)
|
||||
|
||||
// Check *notification* for status in home stream.
|
||||
suite.checkStreamed(
|
||||
homeStream,
|
||||
true,
|
||||
string(notifJSON),
|
||||
stream.EventTypeNotification,
|
||||
)
|
||||
|
||||
// Status itself should not be in home stream.
|
||||
suite.checkStreamed(
|
||||
homeStream,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
// Updating a public status with a hashtag followed by a local user who does not otherwise follow the author
|
||||
// should stream a status update to the tag-following user's home timeline.
|
||||
func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag() {
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user