diff options
Diffstat (limited to 'internal/visibility')
-rw-r--r-- | internal/visibility/account.go | 154 | ||||
-rw-r--r-- | internal/visibility/boostable.go | 62 | ||||
-rw-r--r-- | internal/visibility/boostable_test.go | 154 | ||||
-rw-r--r-- | internal/visibility/filter.go | 37 | ||||
-rw-r--r-- | internal/visibility/filter_test.go | 77 | ||||
-rw-r--r-- | internal/visibility/home_timeline.go | 279 | ||||
-rw-r--r-- | internal/visibility/home_timeline_test.go | 415 | ||||
-rw-r--r-- | internal/visibility/public_timeline.go | 123 | ||||
-rw-r--r-- | internal/visibility/status.go | 213 | ||||
-rw-r--r-- | internal/visibility/status_test.go | 161 | ||||
-rw-r--r-- | internal/visibility/tag_timeline.go | 60 |
11 files changed, 0 insertions, 1735 deletions
diff --git a/internal/visibility/account.go b/internal/visibility/account.go deleted file mode 100644 index 410daa1ce..000000000 --- a/internal/visibility/account.go +++ /dev/null @@ -1,154 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility - -import ( - "context" - - "github.com/superseriousbusiness/gotosocial/internal/cache" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -// AccountVisible will check if given account is visible to requester, accounting for requester with no auth (i.e is nil), suspensions, disabled local users and account blocks. -func (f *Filter) AccountVisible(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (bool, error) { - const vtype = cache.VisibilityTypeAccount - - // By default we assume no auth. - requesterID := noauth - - if requester != nil { - // Use provided account ID. - requesterID = requester.ID - } - - visibility, err := f.state.Caches.Visibility.LoadOne("Type,RequesterID,ItemID", func() (*cache.CachedVisibility, error) { - // Visibility not yet cached, perform visibility lookup. - visible, err := f.isAccountVisibleTo(ctx, requester, account) - if err != nil { - return nil, err - } - - // Return visibility value. - return &cache.CachedVisibility{ - ItemID: account.ID, - RequesterID: requesterID, - Type: vtype, - Value: visible, - }, nil - }, vtype, requesterID, account.ID) - if err != nil { - return false, err - } - - return visibility.Value, nil -} - -// isAccountVisibleTo will check if account is visible to requester. It is the "meat" of the logic to Filter{}.AccountVisible() which is called within cache loader callback. -func (f *Filter) isAccountVisibleTo(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (bool, error) { - // Check whether target account is visible to anyone. - visible, err := f.isAccountVisible(ctx, account) - if err != nil { - return false, gtserror.Newf("error checking account %s visibility: %w", account.ID, err) - } - - if !visible { - log.Trace(ctx, "target account is not visible to anyone") - return false, nil - } - - if requester == nil { - // It seems stupid, but when un-authed all accounts are - // visible to allow for federation to work correctly. - return true, nil - } - - // If requester is not visible, they cannot *see* either. - visible, err = f.isAccountVisible(ctx, requester) - if err != nil { - return false, gtserror.Newf("error checking account %s visibility: %w", account.ID, err) - } - - if !visible { - log.Trace(ctx, "requesting account cannot see other accounts") - return false, nil - } - - // Check whether either blocks the other. - blocked, err := f.state.DB.IsEitherBlocked(ctx, - requester.ID, - account.ID, - ) - if err != nil { - return false, gtserror.Newf("error checking account blocks: %w", err) - } - - if blocked { - log.Trace(ctx, "block exists between accounts") - return false, nil - } - - return true, nil -} - -// isAccountVisible will check if given account should be visible at all, e.g. it may not be if suspended or disabled. -func (f *Filter) isAccountVisible(ctx context.Context, account *gtsmodel.Account) (bool, error) { - if account.IsLocal() { - // This is a local account. - - if account.Username == config.GetHost() { - // This is the instance actor account. - return true, nil - } - - // Fetch the local user model for this account. - user, err := f.state.DB.GetUserByAccountID(ctx, account.ID) - if err != nil { - err := gtserror.Newf("db error getting user for account %s: %w", account.ID, err) - return false, err - } - - // Make sure that user is active (i.e. not disabled, not approved etc). - if *user.Disabled || !*user.Approved || user.ConfirmedAt.IsZero() { - log.Trace(ctx, "local account not active") - return false, nil - } - } else { - // This is a remote account. - - // Check whether remote account's domain is blocked. - blocked, err := f.state.DB.IsDomainBlocked(ctx, account.Domain) - if err != nil { - return false, err - } - - if blocked { - log.Trace(ctx, "remote account domain blocked") - return false, nil - } - } - - if !account.SuspendedAt.IsZero() { - log.Trace(ctx, "account suspended") - return false, nil - } - - return true, nil -} diff --git a/internal/visibility/boostable.go b/internal/visibility/boostable.go deleted file mode 100644 index 7c8bda324..000000000 --- a/internal/visibility/boostable.go +++ /dev/null @@ -1,62 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility - -import ( - "context" - - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -// StatusBoostable checks if given status is boostable by requester, checking boolean status visibility to requester and ultimately the AP status visibility setting. -func (f *Filter) StatusBoostable(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - if status.Visibility == gtsmodel.VisibilityDirect { - log.Trace(ctx, "direct statuses are not boostable") - return false, nil - } - - // Check whether status is visible to requesting account. - visible, err := f.StatusVisible(ctx, requester, status) - if err != nil { - return false, err - } - - if !visible { - log.Trace(ctx, "status not visible to requesting account") - return false, nil - } - - if requester.ID == status.AccountID { - // Status author can always boost non-directs. - return true, nil - } - - if status.Visibility == gtsmodel.VisibilityFollowersOnly || - status.Visibility == gtsmodel.VisibilityMutualsOnly { - log.Trace(ctx, "unauthored %s status not boostable", status.Visibility) - return false, nil - } - - if !*status.Boostable { - log.Trace(ctx, "status marked not boostable") - return false, nil - } - - return true, nil -} diff --git a/internal/visibility/boostable_test.go b/internal/visibility/boostable_test.go deleted file mode 100644 index fd29e7305..000000000 --- a/internal/visibility/boostable_test.go +++ /dev/null @@ -1,154 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/suite" -) - -type StatusBoostableTestSuite struct { - FilterStandardTestSuite -} - -func (suite *StatusBoostableTestSuite) TestOwnPublicBoostable() { - testStatus := suite.testStatuses["local_account_1_status_1"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOwnUnlockedBoostable() { - testStatus := suite.testStatuses["local_account_1_status_2"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOwnMutualsOnlyNonInteractiveBoostable() { - testStatus := suite.testStatuses["local_account_1_status_3"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOwnMutualsOnlyBoostable() { - testStatus := suite.testStatuses["local_account_1_status_4"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOwnFollowersOnlyBoostable() { - testStatus := suite.testStatuses["local_account_1_status_5"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOwnDirectNotBoostable() { - testStatus := suite.testStatuses["local_account_2_status_6"] - testAccount := suite.testAccounts["local_account_2"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOtherPublicBoostable() { - testStatus := suite.testStatuses["local_account_2_status_1"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOtherUnlistedBoostable() { - testStatus := suite.testStatuses["local_account_1_status_2"] - testAccount := suite.testAccounts["local_account_2"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOtherFollowersOnlyNotBoostable() { - testStatus := suite.testStatuses["local_account_2_status_7"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(boostable) -} - -func (suite *StatusBoostableTestSuite) TestOtherDirectNotBoostable() { - testStatus := suite.testStatuses["local_account_2_status_6"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(boostable) -} - -func (suite *StatusBoostableTestSuite) TestRemoteFollowersOnlyNotVisible() { - testStatus := suite.testStatuses["local_account_1_status_5"] - testAccount := suite.testAccounts["remote_account_1"] - ctx := context.Background() - - boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(boostable) -} - -func TestStatusBoostableTestSuite(t *testing.T) { - suite.Run(t, new(StatusBoostableTestSuite)) -} diff --git a/internal/visibility/filter.go b/internal/visibility/filter.go deleted file mode 100644 index c9f007ccf..000000000 --- a/internal/visibility/filter.go +++ /dev/null @@ -1,37 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility - -import ( - "github.com/superseriousbusiness/gotosocial/internal/state" -) - -// noauth is a placeholder ID used in cache lookups -// when there is no authorized account ID to use. -const noauth = "noauth" - -// Filter packages up a bunch of logic for checking whether -// given statuses or accounts are visible to a requester. -type Filter struct { - state *state.State -} - -// NewFilter returns a new Filter interface that will use the provided database. -func NewFilter(state *state.State) *Filter { - return &Filter{state: state} -} diff --git a/internal/visibility/filter_test.go b/internal/visibility/filter_test.go deleted file mode 100644 index 41f06079a..000000000 --- a/internal/visibility/filter_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility_test - -import ( - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/state" - "github.com/superseriousbusiness/gotosocial/internal/visibility" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type FilterStandardTestSuite struct { - // standard suite interfaces - suite.Suite - db db.DB - state state.State - - // standard suite models - testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client - testApplications map[string]*gtsmodel.Application - testUsers map[string]*gtsmodel.User - testAccounts map[string]*gtsmodel.Account - testAttachments map[string]*gtsmodel.MediaAttachment - testStatuses map[string]*gtsmodel.Status - testTags map[string]*gtsmodel.Tag - testMentions map[string]*gtsmodel.Mention - testFollows map[string]*gtsmodel.Follow - - filter *visibility.Filter -} - -func (suite *FilterStandardTestSuite) SetupSuite() { - suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() - suite.testApplications = testrig.NewTestApplications() - suite.testUsers = testrig.NewTestUsers() - suite.testAccounts = testrig.NewTestAccounts() - suite.testAttachments = testrig.NewTestAttachments() - suite.testStatuses = testrig.NewTestStatuses() - suite.testTags = testrig.NewTestTags() - suite.testMentions = testrig.NewTestMentions() - suite.testFollows = testrig.NewTestFollows() -} - -func (suite *FilterStandardTestSuite) SetupTest() { - suite.state.Caches.Init() - - testrig.InitTestConfig() - testrig.InitTestLog() - - suite.db = testrig.NewTestDB(&suite.state) - suite.filter = visibility.NewFilter(&suite.state) - - testrig.StandardDBSetup(suite.db, nil) -} - -func (suite *FilterStandardTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) -} diff --git a/internal/visibility/home_timeline.go b/internal/visibility/home_timeline.go deleted file mode 100644 index 0a3fbde4e..000000000 --- a/internal/visibility/home_timeline.go +++ /dev/null @@ -1,279 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility - -import ( - "context" - "errors" - "time" - - "github.com/superseriousbusiness/gotosocial/internal/cache" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtscontext" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -// StatusHomeTimelineable checks if given status should be included on owner's home timeline. Primarily relying on status visibility to owner and the AP visibility setting, but also taking into account thread replies etc. -func (f *Filter) StatusHomeTimelineable(ctx context.Context, owner *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - const vtype = cache.VisibilityTypeHome - - // By default we assume no auth. - requesterID := noauth - - if owner != nil { - // Use provided account ID. - requesterID = owner.ID - } - - visibility, err := f.state.Caches.Visibility.LoadOne("Type,RequesterID,ItemID", func() (*cache.CachedVisibility, error) { - // Visibility not yet cached, perform timeline visibility lookup. - visible, err := f.isStatusHomeTimelineable(ctx, owner, status) - if err != nil { - return nil, err - } - - // Return visibility value. - return &cache.CachedVisibility{ - ItemID: status.ID, - RequesterID: requesterID, - Type: vtype, - Value: visible, - }, nil - }, vtype, requesterID, status.ID) - if err != nil { - if err == cache.SentinelError { - // Filter-out our temporary - // race-condition error. - return false, nil - } - - return false, err - } - - return visibility.Value, nil -} - -func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - if status.CreatedAt.After(time.Now().Add(24 * time.Hour)) { - // Statuses made over 1 day in the future we don't show... - log.Warnf(ctx, "status >24hrs in the future: %+v", status) - return false, nil - } - - // Check whether status is visible to timeline owner. - visible, err := f.StatusVisible(ctx, owner, status) - if err != nil { - return false, err - } - - if !visible { - log.Trace(ctx, "status not visible to timeline owner") - return false, nil - } - - if status.AccountID == owner.ID { - // Author can always see their status. - return true, nil - } - - if status.MentionsAccount(owner.ID) { - // Can always see when you are mentioned. - return true, nil - } - - var ( - // iterated-over - // loop status. - next = status - - // assume one author - // until proven otherwise. - oneAuthor = true - ) - - for { - // Populate account mention objects before account mention checks. - next.Mentions, err = f.state.DB.GetMentions(ctx, next.MentionIDs) - if err != nil { - return false, gtserror.Newf("error populating status %s mentions: %w", next.ID, err) - } - - if (next.AccountID == owner.ID) || - next.MentionsAccount(owner.ID) { - // Owner is in / mentioned in - // this status thread. They can - // see future visible statuses. - visible = true - break - } - - var notVisible bool - - // Check whether status in conversation is explicitly relevant to timeline - // owner (i.e. includes mutals), or is explicitly invisible (i.e. blocked). - visible, notVisible, err = f.isVisibleConversation(ctx, owner, next) - if err != nil { - return false, gtserror.Newf("error checking conversation visibility: %w", err) - } - - if notVisible { - log.Tracef(ctx, "conversation not visible to timeline owner") - return false, nil - } - - if visible { - // Conversation relevant - // to timeline owner! - break - } - - if oneAuthor { - // Check if this continues to be a single-author thread. - oneAuthor = (next.AccountID == status.AccountID) - } - - if next.InReplyToURI == "" { - // Reached the top of the thread. - break - } - - // Check parent is deref'd. - if next.InReplyToID == "" { - log.Debugf(ctx, "status not (yet) deref'd: %s", next.InReplyToURI) - return false, cache.SentinelError - } - - // Fetch next parent in conversation. - next, err = f.state.DB.GetStatusByID( - gtscontext.SetBarebones(ctx), - next.InReplyToID, - ) - if err != nil { - return false, gtserror.Newf("error getting status parent %s: %w", next.InReplyToID, err) - } - } - - if next != status && !oneAuthor && !visible { - log.Trace(ctx, "ignoring visible reply in conversation irrelevant to owner") - return false, nil - } - - // At this point status is either a top-level status, a reply in a single - // author thread (e.g. "this is my weird-ass take and here is why 1/10 🧵"), - // a status thread *including* the owner, or a conversation thread between - // accounts the timeline owner follows. - - // Ensure owner follows author. - follow, err := f.state.DB.GetFollow(ctx, - owner.ID, - status.AccountID, - ) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - return false, gtserror.Newf("error retrieving follow %s->%s: %w", owner.ID, status.AccountID, err) - } - - if follow == nil { - log.Trace(ctx, "ignoring status from unfollowed author") - return false, nil - } - - if status.BoostOfID != "" && !*follow.ShowReblogs { - // Status is a boost, but the owner of this follow - // doesn't want to see boosts from this account. - return false, nil - } - - return true, nil -} - -func (f *Filter) isVisibleConversation( - ctx context.Context, - owner *gtsmodel.Account, - status *gtsmodel.Status, -) ( - bool, // explicitly IS visible - bool, // explicitly NOT visible - error, // err -) { - // Check if status is visible to the timeline owner. - visible, err := f.StatusVisible(ctx, owner, status) - if err != nil { - return false, false, err - } - - if !visible { - // Explicitly NOT visible - // to the timeline owner. - return false, true, nil - } - - if status.Visibility == gtsmodel.VisibilityUnlocked || - status.Visibility == gtsmodel.VisibilityPublic { - // NOTE: there is no need to check in the case of - // direct / follow-only / mutual-only visibility statuses - // as the above visibility check already handles this. - - // Check owner follows the status author. - follow, err := f.state.DB.IsFollowing(ctx, - owner.ID, - status.AccountID, - ) - if err != nil { - return false, false, gtserror.Newf("error checking follow %s->%s: %w", owner.ID, status.AccountID, err) - } - - if !follow { - // Not explicitly visible - // status to timeline owner. - return false, false, nil - } - } - - var follow bool - - for _, mention := range status.Mentions { - // Check block between timeline owner and mention. - block, err := f.state.DB.IsEitherBlocked(ctx, - owner.ID, - mention.TargetAccountID, - ) - if err != nil { - return false, false, gtserror.Newf("error checking mention block %s<->%s: %w", owner.ID, mention.TargetAccountID, err) - } - - if block { - // Invisible conversation. - return false, true, nil - } - - if !follow { - // See if tl owner follows any of mentions. - follow, err = f.state.DB.IsFollowing(ctx, - owner.ID, - mention.TargetAccountID, - ) - if err != nil { - return false, false, gtserror.Newf("error checking mention follow %s->%s: %w", owner.ID, mention.TargetAccountID, err) - } - } - } - - return follow, false, nil -} diff --git a/internal/visibility/home_timeline_test.go b/internal/visibility/home_timeline_test.go deleted file mode 100644 index d8211c8dd..000000000 --- a/internal/visibility/home_timeline_test.go +++ /dev/null @@ -1,415 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type StatusStatusHomeTimelineableTestSuite struct { - FilterStandardTestSuite -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestOwnStatusHomeTimelineable() { - testStatus := suite.testStatuses["local_account_1_status_1"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - timelineable, err := suite.filter.StatusHomeTimelineable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(timelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestFollowingStatusHomeTimelineable() { - testStatus := suite.testStatuses["local_account_2_status_1"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - timelineable, err := suite.filter.StatusHomeTimelineable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(timelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestFollowingBoostedStatusHomeTimelineable() { - ctx := context.Background() - - testStatus := suite.testStatuses["admin_account_status_4"] - testAccount := suite.testAccounts["local_account_1"] - timelineable, err := suite.filter.StatusHomeTimelineable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(timelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestFollowingBoostedStatusHomeTimelineableNoReblogs() { - ctx := context.Background() - - // Update follow to indicate that local_account_1 - // doesn't want to see reblogs by admin_account. - follow := >smodel.Follow{} - *follow = *suite.testFollows["local_account_1_admin_account"] - follow.ShowReblogs = util.Ptr(false) - - if err := suite.db.UpdateFollow(ctx, follow, "show_reblogs"); err != nil { - suite.FailNow(err.Error()) - } - - testStatus := suite.testStatuses["admin_account_status_4"] - testAccount := suite.testAccounts["local_account_1"] - timelineable, err := suite.filter.StatusHomeTimelineable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(timelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestNotFollowingStatusHomeTimelineable() { - testStatus := suite.testStatuses["remote_account_1_status_1"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - timelineable, err := suite.filter.StatusHomeTimelineable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(timelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestStatusTooNewNotTimelineable() { - testStatus := >smodel.Status{} - *testStatus = *suite.testStatuses["local_account_1_status_1"] - - testStatus.CreatedAt = time.Now().Add(25 * time.Hour) - - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - timelineable, err := suite.filter.StatusHomeTimelineable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(timelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestStatusNotTooNewTimelineable() { - testStatus := >smodel.Status{} - *testStatus = *suite.testStatuses["local_account_1_status_1"] - - testStatus.CreatedAt = time.Now().Add(23 * time.Hour) - - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - timelineable, err := suite.filter.StatusHomeTimelineable(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(timelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestThread() { - ctx := context.Background() - - threadParentAccount := suite.testAccounts["local_account_1"] - timelineOwnerAccount := suite.testAccounts["local_account_2"] - originalStatus := suite.testStatuses["local_account_1_status_1"] - - // this status should be hometimelineable for local_account_2 - originalStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, originalStatus) - suite.NoError(err) - suite.True(originalStatusTimelineable) - - // now a reply from the original status author to their own status - firstReplyStatus := >smodel.Status{ - ID: "01G395ESAYPK9161QSQEZKATJN", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/01G395ESAYPK9161QSQEZKATJN", - URL: "http://localhost:8080/@the_mighty_zork/statuses/01G395ESAYPK9161QSQEZKATJN", - Content: "nbnbdy expects dog", - CreatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"), - UpdatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"), - Local: util.Ptr(false), - AccountURI: "http://localhost:8080/users/the_mighty_zork", - AccountID: threadParentAccount.ID, - InReplyToID: originalStatus.ID, - InReplyToAccountID: threadParentAccount.ID, - InReplyToURI: originalStatus.URI, - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityFollowersOnly, - Sensitive: util.Ptr(false), - Language: "en", - CreatedWithApplicationID: "", - Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, - } - if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { - suite.FailNow(err.Error()) - } - - // this status should also be hometimelineable for local_account_2 - firstReplyStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, firstReplyStatus) - suite.NoError(err) - suite.True(firstReplyStatusTimelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly() { - ctx := context.Background() - - // This scenario makes sure that we don't timeline a status which is a followers-only - // reply to a followers-only status TO A FOLLOWERS-ONLY STATUS owned by someone the - // timeline owner account doesn't follow. - // - // In other words, remote_account_1 posts a followers-only status, which local_account_1 replies to; - // THEN, local_account_1 replies to their own reply. None of these statuses should appear to - // local_account_2 since they don't follow the original parent. - // - // See: https://github.com/superseriousbusiness/gotosocial/issues/501 - - originalStatusParent := suite.testAccounts["remote_account_1"] - replyingAccount := suite.testAccounts["local_account_1"] - timelineOwnerAccount := suite.testAccounts["local_account_2"] - - // put a followers-only status by remote_account_1 in the db - originalStatus := >smodel.Status{ - ID: "01G3957TS7XE2CMDKFG3MZPWAF", - URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/01G3957TS7XE2CMDKFG3MZPWAF", - URL: "http://fossbros-anonymous.io/@foss_satan/statuses/01G3957TS7XE2CMDKFG3MZPWAF", - Content: "didn't expect dog", - CreatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"), - UpdatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"), - Local: util.Ptr(false), - AccountURI: "http://fossbros-anonymous.io/users/foss_satan", - AccountID: originalStatusParent.ID, - InReplyToID: "", - InReplyToAccountID: "", - InReplyToURI: "", - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityFollowersOnly, - Sensitive: util.Ptr(false), - Language: "en", - CreatedWithApplicationID: "", - Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, - } - if err := suite.db.PutStatus(ctx, originalStatus); err != nil { - suite.FailNow(err.Error()) - } - // this status should not be hometimelineable for local_account_2 - originalStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, originalStatus) - suite.NoError(err) - suite.False(originalStatusTimelineable) - - // now a followers-only reply from zork - firstReplyStatus := >smodel.Status{ - ID: "01G395ESAYPK9161QSQEZKATJN", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/01G395ESAYPK9161QSQEZKATJN", - URL: "http://localhost:8080/@the_mighty_zork/statuses/01G395ESAYPK9161QSQEZKATJN", - Content: "nbnbdy expects dog", - CreatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"), - UpdatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"), - Local: util.Ptr(false), - AccountURI: "http://localhost:8080/users/the_mighty_zork", - AccountID: replyingAccount.ID, - InReplyToID: originalStatus.ID, - InReplyToAccountID: originalStatusParent.ID, - InReplyToURI: originalStatus.URI, - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityFollowersOnly, - Sensitive: util.Ptr(false), - Language: "en", - CreatedWithApplicationID: "", - Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, - } - if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { - suite.FailNow(err.Error()) - } - // this status should be hometimelineable for local_account_2 - firstReplyStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, firstReplyStatus) - suite.NoError(err) - suite.False(firstReplyStatusTimelineable) - - // now a followers-only reply from zork to the status they just replied to - secondReplyStatus := >smodel.Status{ - ID: "01G395NZQZGJYRBAES57KYZ7XP", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/01G395NZQZGJYRBAES57KYZ7XP", - URL: "http://localhost:8080/@the_mighty_zork/statuses/01G395NZQZGJYRBAES57KYZ7XP", - Content: "*nobody", - CreatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"), - UpdatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"), - Local: util.Ptr(false), - AccountURI: "http://localhost:8080/users/the_mighty_zork", - AccountID: replyingAccount.ID, - InReplyToID: firstReplyStatus.ID, - InReplyToAccountID: replyingAccount.ID, - InReplyToURI: firstReplyStatus.URI, - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityFollowersOnly, - Sensitive: util.Ptr(false), - Language: "en", - CreatedWithApplicationID: "", - Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, - } - if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil { - suite.FailNow(err.Error()) - } - - // this status should ALSO not be hometimelineable for local_account_2 - secondReplyStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, secondReplyStatus) - suite.NoError(err) - suite.False(secondReplyStatusTimelineable) -} - -func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnlocked() { - ctx := context.Background() - - // This scenario is exactly the same as the above test, but for a mix of unlocked + public posts - - originalStatusParent := suite.testAccounts["remote_account_1"] - replyingAccount := suite.testAccounts["local_account_1"] - timelineOwnerAccount := suite.testAccounts["local_account_2"] - - // put an unlocked status by remote_account_1 in the db - originalStatus := >smodel.Status{ - ID: "01G3957TS7XE2CMDKFG3MZPWAF", - URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/01G3957TS7XE2CMDKFG3MZPWAF", - URL: "http://fossbros-anonymous.io/@foss_satan/statuses/01G3957TS7XE2CMDKFG3MZPWAF", - Content: "didn't expect dog", - CreatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"), - UpdatedAt: testrig.TimeMustParse("2021-09-20T12:40:37+02:00"), - Local: util.Ptr(false), - AccountURI: "http://fossbros-anonymous.io/users/foss_satan", - AccountID: originalStatusParent.ID, - InReplyToID: "", - InReplyToAccountID: "", - InReplyToURI: "", - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityUnlocked, - Sensitive: util.Ptr(false), - Language: "en", - CreatedWithApplicationID: "", - Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, - } - if err := suite.db.PutStatus(ctx, originalStatus); err != nil { - suite.FailNow(err.Error()) - } - // this status should not be hometimelineable for local_account_2 - originalStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, originalStatus) - suite.NoError(err) - suite.False(originalStatusTimelineable) - - // now a public reply from zork - firstReplyStatus := >smodel.Status{ - ID: "01G395ESAYPK9161QSQEZKATJN", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/01G395ESAYPK9161QSQEZKATJN", - URL: "http://localhost:8080/@the_mighty_zork/statuses/01G395ESAYPK9161QSQEZKATJN", - Content: "nbnbdy expects dog", - CreatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"), - UpdatedAt: testrig.TimeMustParse("2021-09-20T12:41:37+02:00"), - Local: util.Ptr(false), - AccountURI: "http://localhost:8080/users/the_mighty_zork", - AccountID: replyingAccount.ID, - InReplyToID: originalStatus.ID, - InReplyToAccountID: originalStatusParent.ID, - InReplyToURI: originalStatus.URI, - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityPublic, - Sensitive: util.Ptr(false), - Language: "en", - CreatedWithApplicationID: "", - Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, - } - if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { - suite.FailNow(err.Error()) - } - // this status should not be hometimelineable for local_account_2 - firstReplyStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, firstReplyStatus) - suite.NoError(err) - suite.False(firstReplyStatusTimelineable) - - // now an unlocked reply from zork to the status they just replied to - secondReplyStatus := >smodel.Status{ - ID: "01G395NZQZGJYRBAES57KYZ7XP", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/01G395NZQZGJYRBAES57KYZ7XP", - URL: "http://localhost:8080/@the_mighty_zork/statuses/01G395NZQZGJYRBAES57KYZ7XP", - Content: "*nobody", - CreatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"), - UpdatedAt: testrig.TimeMustParse("2021-09-20T12:42:37+02:00"), - Local: util.Ptr(false), - AccountURI: "http://localhost:8080/users/the_mighty_zork", - AccountID: replyingAccount.ID, - InReplyToID: firstReplyStatus.ID, - InReplyToAccountID: replyingAccount.ID, - InReplyToURI: firstReplyStatus.URI, - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityUnlocked, - Sensitive: util.Ptr(false), - Language: "en", - CreatedWithApplicationID: "", - Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, - } - if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil { - suite.FailNow(err.Error()) - } - - // this status should ALSO not be hometimelineable for local_account_2 - secondReplyStatusTimelineable, err := suite.filter.StatusHomeTimelineable(ctx, timelineOwnerAccount, secondReplyStatus) - suite.NoError(err) - suite.False(secondReplyStatusTimelineable) -} - -func TestStatusHomeTimelineableTestSuite(t *testing.T) { - suite.Run(t, new(StatusStatusHomeTimelineableTestSuite)) -} diff --git a/internal/visibility/public_timeline.go b/internal/visibility/public_timeline.go deleted file mode 100644 index bad7cf991..000000000 --- a/internal/visibility/public_timeline.go +++ /dev/null @@ -1,123 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility - -import ( - "context" - "time" - - "github.com/superseriousbusiness/gotosocial/internal/cache" - "github.com/superseriousbusiness/gotosocial/internal/gtscontext" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -// StatusHomeTimelineable checks if given status should be included on requester's public timeline. Primarily relying on status visibility to requester and the AP visibility setting, and ignoring conversation threads. -func (f *Filter) StatusPublicTimelineable(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - const vtype = cache.VisibilityTypePublic - - // By default we assume no auth. - requesterID := noauth - - if requester != nil { - // Use provided account ID. - requesterID = requester.ID - } - - visibility, err := f.state.Caches.Visibility.LoadOne("Type,RequesterID,ItemID", func() (*cache.CachedVisibility, error) { - // Visibility not yet cached, perform timeline visibility lookup. - visible, err := f.isStatusPublicTimelineable(ctx, requester, status) - if err != nil { - return nil, err - } - - // Return visibility value. - return &cache.CachedVisibility{ - ItemID: status.ID, - RequesterID: requesterID, - Type: vtype, - Value: visible, - }, nil - }, vtype, requesterID, status.ID) - if err != nil { - if err == cache.SentinelError { - // Filter-out our temporary - // race-condition error. - return false, nil - } - - return false, err - } - - return visibility.Value, nil -} - -func (f *Filter) isStatusPublicTimelineable(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - if status.CreatedAt.After(time.Now().Add(24 * time.Hour)) { - // Statuses made over 1 day in the future we don't show... - log.Warnf(ctx, "status >24hrs in the future: %+v", status) - return false, nil - } - - // Don't show boosts on timeline. - if status.BoostOfID != "" { - return false, nil - } - - // Check whether status is visible to requesting account. - visible, err := f.StatusVisible(ctx, requester, status) - if err != nil { - return false, err - } - - if !visible { - log.Trace(ctx, "status not visible to timeline requester") - return false, nil - } - - for parent := status; parent.InReplyToURI != ""; { - // Fetch next parent to lookup. - parentID := parent.InReplyToID - if parentID == "" { - log.Debugf(ctx, "status not (yet) deref'd: %s", parent.InReplyToURI) - return false, cache.SentinelError - } - - // Get the next parent in the chain from DB. - parent, err = f.state.DB.GetStatusByID( - gtscontext.SetBarebones(ctx), - parentID, - ) - if err != nil { - return false, gtserror.Newf("error getting status parent %s: %w", parentID, err) - } - - if parent.AccountID != status.AccountID { - // This is not a single author reply-chain-thread, - // instead is an actualy conversation. Don't timeline. - log.Trace(ctx, "ignoring multi-author reply-chain") - return false, nil - } - } - - // This is either a visible status in a - // single-author thread, or a visible top - // level status. Show on public timeline. - return true, nil -} diff --git a/internal/visibility/status.go b/internal/visibility/status.go deleted file mode 100644 index 5e2052ae4..000000000 --- a/internal/visibility/status.go +++ /dev/null @@ -1,213 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility - -import ( - "context" - "slices" - - "github.com/superseriousbusiness/gotosocial/internal/cache" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -// StatusesVisible calls StatusVisible for each status in the statuses slice, and returns a slice of only statuses which are visible to the requester. -func (f *Filter) StatusesVisible(ctx context.Context, requester *gtsmodel.Account, statuses []*gtsmodel.Status) ([]*gtsmodel.Status, error) { - var errs gtserror.MultiError - filtered := slices.DeleteFunc(statuses, func(status *gtsmodel.Status) bool { - visible, err := f.StatusVisible(ctx, requester, status) - if err != nil { - errs.Append(err) - return true - } - return !visible - }) - return filtered, errs.Combine() -} - -// StatusVisible will check if given status is visible to requester, accounting for requester with no auth (i.e is nil), suspensions, disabled local users, account blocks and status privacy. -func (f *Filter) StatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - const vtype = cache.VisibilityTypeStatus - - // By default we assume no auth. - requesterID := noauth - - if requester != nil { - // Use provided account ID. - requesterID = requester.ID - } - - visibility, err := f.state.Caches.Visibility.LoadOne("Type,RequesterID,ItemID", func() (*cache.CachedVisibility, error) { - // Visibility not yet cached, perform visibility lookup. - visible, err := f.isStatusVisible(ctx, requester, status) - if err != nil { - return nil, err - } - - // Return visibility value. - return &cache.CachedVisibility{ - ItemID: status.ID, - RequesterID: requesterID, - Type: vtype, - Value: visible, - }, nil - }, vtype, requesterID, status.ID) - if err != nil { - return false, err - } - - return visibility.Value, nil -} - -// isStatusVisible will check if status is visible to requester. It is the "meat" of the logic to Filter{}.StatusVisible() which is called within cache loader callback. -func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - // Ensure that status is fully populated for further processing. - if err := f.state.DB.PopulateStatus(ctx, status); err != nil { - return false, gtserror.Newf("error populating status %s: %w", status.ID, err) - } - - // Check whether status accounts are visible to the requester. - visible, err := f.areStatusAccountsVisible(ctx, requester, status) - if err != nil { - return false, gtserror.Newf("error checking status %s account visibility: %w", status.ID, err) - } else if !visible { - return false, nil - } - - if status.Visibility == gtsmodel.VisibilityPublic { - // This status will be visible to all. - return true, nil - } - - if requester == nil { - // This request is WITHOUT auth, and status is NOT public. - log.Trace(ctx, "unauthorized request to non-public status") - return false, nil - } - - if status.Visibility == gtsmodel.VisibilityUnlocked { - // This status is visible to all auth'd accounts. - return true, nil - } - - if requester.ID == status.AccountID { - // Author can always see their own status. - return true, nil - } - - if status.MentionsAccount(requester.ID) { - // Status mentions the requesting account. - return true, nil - } - - if status.BoostOf != nil { - if !status.BoostOf.MentionsPopulated() { - // Boosted status needs its mentions populating, fetch these from database. - status.BoostOf.Mentions, err = f.state.DB.GetMentions(ctx, status.BoostOf.MentionIDs) - if err != nil { - return false, gtserror.Newf("error populating boosted status %s mentions: %w", status.BoostOfID, err) - } - } - - if status.BoostOf.MentionsAccount(requester.ID) { - // Boosted status mentions the requesting account. - return true, nil - } - } - - switch status.Visibility { - case gtsmodel.VisibilityFollowersOnly: - // Check requester follows status author. - follows, err := f.state.DB.IsFollowing(ctx, - requester.ID, - status.AccountID, - ) - if err != nil { - return false, gtserror.Newf("error checking follow %s->%s: %w", requester.ID, status.AccountID, err) - } - - if !follows { - log.Trace(ctx, "follow-only status not visible to requester") - return false, nil - } - - return true, nil - - case gtsmodel.VisibilityMutualsOnly: - // Check mutual following between requester and author. - mutuals, err := f.state.DB.IsMutualFollowing(ctx, - requester.ID, - status.AccountID, - ) - if err != nil { - return false, gtserror.Newf("error checking mutual follow %s<->%s: %w", requester.ID, status.AccountID, err) - } - - if !mutuals { - log.Trace(ctx, "mutual-only status not visible to requester") - return false, nil - } - - return true, nil - - case gtsmodel.VisibilityDirect: - log.Trace(ctx, "direct status not visible to requester") - return false, nil - - default: - log.Warnf(ctx, "unexpected status visibility %s for %s", status.Visibility, status.URI) - return false, nil - } -} - -// areStatusAccountsVisible calls Filter{}.AccountVisible() on status author and the status boost-of (if set) author, returning visibility of status (and boost-of) to requester. -func (f *Filter) areStatusAccountsVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { - // Check whether status author's account is visible to requester. - visible, err := f.AccountVisible(ctx, requester, status.Account) - if err != nil { - return false, gtserror.Newf("error checking status author visibility: %w", err) - } - - if !visible { - log.Trace(ctx, "status author not visible to requester") - return false, nil - } - - if status.BoostOfID != "" { - // This is a boosted status. - - if status.AccountID == status.BoostOfAccountID { - // Some clout-chaser boosted their own status, tch. - return true, nil - } - - // Check whether boosted status author's account is visible to requester. - visible, err := f.AccountVisible(ctx, requester, status.BoostOfAccount) - if err != nil { - return false, gtserror.Newf("error checking boosted author visibility: %w", err) - } - - if !visible { - log.Trace(ctx, "boosted status author not visible to requester") - return false, nil - } - } - - return true, nil -} diff --git a/internal/visibility/status_test.go b/internal/visibility/status_test.go deleted file mode 100644 index ad6bc66df..000000000 --- a/internal/visibility/status_test.go +++ /dev/null @@ -1,161 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -type StatusVisibleTestSuite struct { - FilterStandardTestSuite -} - -func (suite *StatusVisibleTestSuite) TestOwnStatusVisible() { - testStatus := suite.testStatuses["local_account_1_status_1"] - testAccount := suite.testAccounts["local_account_1"] - ctx := context.Background() - - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(visible) -} - -func (suite *StatusVisibleTestSuite) TestOwnDMVisible() { - ctx := context.Background() - - testStatusID := suite.testStatuses["local_account_2_status_6"].ID - testStatus, err := suite.db.GetStatusByID(ctx, testStatusID) - suite.NoError(err) - testAccount := suite.testAccounts["local_account_2"] - - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(visible) -} - -func (suite *StatusVisibleTestSuite) TestDMVisibleToTarget() { - ctx := context.Background() - - testStatusID := suite.testStatuses["local_account_2_status_6"].ID - testStatus, err := suite.db.GetStatusByID(ctx, testStatusID) - suite.NoError(err) - testAccount := suite.testAccounts["local_account_1"] - - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.True(visible) -} - -func (suite *StatusVisibleTestSuite) TestDMNotVisibleIfNotMentioned() { - ctx := context.Background() - - testStatusID := suite.testStatuses["local_account_2_status_6"].ID - testStatus, err := suite.db.GetStatusByID(ctx, testStatusID) - suite.NoError(err) - testAccount := suite.testAccounts["admin_account"] - - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(visible) -} - -func (suite *StatusVisibleTestSuite) TestStatusNotVisibleIfNotMutuals() { - ctx := context.Background() - - suite.db.DeleteByID(ctx, suite.testFollows["local_account_2_local_account_1"].ID, >smodel.Follow{}) - - testStatusID := suite.testStatuses["local_account_1_status_4"].ID - testStatus, err := suite.db.GetStatusByID(ctx, testStatusID) - suite.NoError(err) - testAccount := suite.testAccounts["local_account_2"] - - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(visible) -} - -func (suite *StatusVisibleTestSuite) TestStatusNotVisibleIfNotFollowing() { - ctx := context.Background() - - suite.db.DeleteByID(ctx, suite.testFollows["admin_account_local_account_1"].ID, >smodel.Follow{}) - - testStatusID := suite.testStatuses["local_account_1_status_5"].ID - testStatus, err := suite.db.GetStatusByID(ctx, testStatusID) - suite.NoError(err) - testAccount := suite.testAccounts["admin_account"] - - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - - suite.False(visible) -} - -func (suite *StatusVisibleTestSuite) TestStatusNotVisibleIfNotMutualsCached() { - ctx := context.Background() - testStatusID := suite.testStatuses["local_account_1_status_4"].ID - testStatus, err := suite.db.GetStatusByID(ctx, testStatusID) - suite.NoError(err) - testAccount := suite.testAccounts["local_account_2"] - - // Perform a status visibility check while mutuals, this shsould be true. - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - suite.True(visible) - - err = suite.db.DeleteFollowByID(ctx, suite.testFollows["local_account_2_local_account_1"].ID) - suite.NoError(err) - - // Perform a status visibility check after unfollow, this should be false. - visible, err = suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - suite.False(visible) -} - -func (suite *StatusVisibleTestSuite) TestStatusNotVisibleIfNotFollowingCached() { - ctx := context.Background() - testStatusID := suite.testStatuses["local_account_1_status_5"].ID - testStatus, err := suite.db.GetStatusByID(ctx, testStatusID) - suite.NoError(err) - testAccount := suite.testAccounts["admin_account"] - - // Perform a status visibility check while following, this shsould be true. - visible, err := suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - suite.True(visible) - - err = suite.db.DeleteFollowByID(ctx, suite.testFollows["admin_account_local_account_1"].ID) - suite.NoError(err) - - // Perform a status visibility check after unfollow, this should be false. - visible, err = suite.filter.StatusVisible(ctx, testAccount, testStatus) - suite.NoError(err) - suite.False(visible) -} - -func TestStatusVisibleTestSuite(t *testing.T) { - suite.Run(t, new(StatusVisibleTestSuite)) -} diff --git a/internal/visibility/tag_timeline.go b/internal/visibility/tag_timeline.go deleted file mode 100644 index b2c9dbf29..000000000 --- a/internal/visibility/tag_timeline.go +++ /dev/null @@ -1,60 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package visibility - -import ( - "context" - "time" - - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -// StatusHomeTimelineable checks if given status should be included -// on requester's tag timeline, primarily relying on status visibility -// to requester and the AP visibility setting. -func (f *Filter) StatusTagTimelineable( - ctx context.Context, - requester *gtsmodel.Account, - status *gtsmodel.Status, -) (bool, error) { - if status.CreatedAt.After(time.Now().Add(24 * time.Hour)) { - // Statuses made over 1 day in the future we don't show... - log.Warnf(ctx, "status >24hrs in the future: %+v", status) - return false, nil - } - - // Don't show boosts on tag timeline. - if status.BoostOfID != "" { - return false, nil - } - - // Check whether status is visible to requesting account. - visible, err := f.StatusVisible(ctx, requester, status) - if err != nil { - return false, err - } - - if !visible { - log.Trace(ctx, "status not visible to timeline requester") - return false, nil - } - - // Looks good! - return true, nil -} |