diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index 475804218..42cbf318b 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -36,6 +36,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/cleaner" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/spam" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -190,10 +191,19 @@ var Start action.GTSAction = func(ctx context.Context) error { oauthServer := oauth.New(ctx, dbService) typeConverter := typeutils.NewConverter(state) visFilter := visibility.NewFilter(state) + intFilter := interaction.NewFilter(state) spamFilter := spam.NewFilter(state) federatingDB := federatingdb.New(state, typeConverter, visFilter, spamFilter) transportController := transport.NewController(state, federatingDB, &federation.Clock{}, client) - federator := federation.NewFederator(state, federatingDB, transportController, typeConverter, visFilter, mediaManager) + federator := federation.NewFederator( + state, + federatingDB, + transportController, + typeConverter, + visFilter, + intFilter, + mediaManager, + ) // Decide whether to create a noop email // sender (won't send emails) or a real one. @@ -268,6 +278,8 @@ var Start action.GTSAction = func(ctx context.Context) error { mediaManager, state, emailSender, + visFilter, + intFilter, ) // Initialize the specialized workers pools. diff --git a/internal/api/client/admin/reportsget_test.go b/internal/api/client/admin/reportsget_test.go index ebcd799f8..5dead789a 100644 --- a/internal/api/client/admin/reportsget_test.go +++ b/internal/api/client/admin/reportsget_test.go @@ -532,19 +532,22 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -774,19 +777,22 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -1016,19 +1022,22 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } diff --git a/internal/api/client/statuses/statusboost_test.go b/internal/api/client/statuses/statusboost_test.go index 25aa2ea0f..f6f589a5c 100644 --- a/internal/api/client/statuses/statusboost_test.go +++ b/internal/api/client/statuses/statusboost_test.go @@ -173,43 +173,42 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() { } // try to boost a status that's not boostable / visible to us -// TODO: sort this out with new interaction policies -// func (suite *StatusBoostTestSuite) TestPostUnboostable() { -// t := suite.testTokens["local_account_1"] -// oauthToken := oauth.DBTokenToToken(t) +func (suite *StatusBoostTestSuite) TestPostUnboostable() { + t := suite.testTokens["local_account_1"] + oauthToken := oauth.DBTokenToToken(t) -// targetStatus := suite.testStatuses["local_account_2_status_4"] + targetStatus := suite.testStatuses["local_account_2_status_4"] -// // setup -// recorder := httptest.NewRecorder() -// ctx, _ := testrig.CreateGinTestContext(recorder, nil) -// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) -// ctx.Set(oauth.SessionAuthorizedToken, oauthToken) -// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) -// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) -// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting -// ctx.Request.Header.Set("accept", "application/json") + // setup + recorder := httptest.NewRecorder() + ctx, _ := testrig.CreateGinTestContext(recorder, nil) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + ctx.Set(oauth.SessionAuthorizedToken, oauthToken) + ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) + ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") -// // normally the router would populate these params from the path values, -// // but because we're calling the function directly, we need to set them manually. -// ctx.Params = gin.Params{ -// gin.Param{ -// Key: statuses.IDKey, -// Value: targetStatus.ID, -// }, -// } + // normally the router would populate these params from the path values, + // but because we're calling the function directly, we need to set them manually. + ctx.Params = gin.Params{ + gin.Param{ + Key: statuses.IDKey, + Value: targetStatus.ID, + }, + } -// suite.statusModule.StatusBoostPOSTHandler(ctx) + suite.statusModule.StatusBoostPOSTHandler(ctx) -// // check response -// suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses + // check response + suite.Equal(http.StatusForbidden, recorder.Code) -// result := recorder.Result() -// defer result.Body.Close() -// b, err := ioutil.ReadAll(result.Body) -// suite.NoError(err) -// suite.Equal(`{"error":"Not Found"}`, string(b)) -// } + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + suite.NoError(err) + suite.Equal(`{"error":"Forbidden: you do not have permission to boost this status"}`, string(b)) +} // try to boost a status that's not visible to the user func (suite *StatusBoostTestSuite) TestPostNotVisible() { diff --git a/internal/api/client/statuses/statusfave_test.go b/internal/api/client/statuses/statusfave_test.go index 5a35351e4..d1042b10e 100644 --- a/internal/api/client/statuses/statusfave_test.go +++ b/internal/api/client/statuses/statusfave_test.go @@ -89,43 +89,42 @@ func (suite *StatusFaveTestSuite) TestPostFave() { } // try to fave a status that's not faveable -// TODO: replace this when interaction policies enforced. -// func (suite *StatusFaveTestSuite) TestPostUnfaveable() { -// t := suite.testTokens["local_account_1"] -// oauthToken := oauth.DBTokenToToken(t) +func (suite *StatusFaveTestSuite) TestPostUnfaveable() { + t := suite.testTokens["admin_account"] + oauthToken := oauth.DBTokenToToken(t) -// targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable + targetStatus := suite.testStatuses["local_account_1_status_3"] // this one is unlikeable -// // setup -// recorder := httptest.NewRecorder() -// ctx, _ := testrig.CreateGinTestContext(recorder, nil) -// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) -// ctx.Set(oauth.SessionAuthorizedToken, oauthToken) -// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) -// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) -// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting -// ctx.Request.Header.Set("accept", "application/json") + // setup + recorder := httptest.NewRecorder() + ctx, _ := testrig.CreateGinTestContext(recorder, nil) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + ctx.Set(oauth.SessionAuthorizedToken, oauthToken) + ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["admin_account"]) + ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["admin_account"]) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") -// // normally the router would populate these params from the path values, -// // but because we're calling the function directly, we need to set them manually. -// ctx.Params = gin.Params{ -// gin.Param{ -// Key: statuses.IDKey, -// Value: targetStatus.ID, -// }, -// } + // normally the router would populate these params from the path values, + // but because we're calling the function directly, we need to set them manually. + ctx.Params = gin.Params{ + gin.Param{ + Key: statuses.IDKey, + Value: targetStatus.ID, + }, + } -// suite.statusModule.StatusFavePOSTHandler(ctx) + suite.statusModule.StatusFavePOSTHandler(ctx) -// // check response -// suite.EqualValues(http.StatusForbidden, recorder.Code) + // check response + suite.EqualValues(http.StatusForbidden, recorder.Code) -// result := recorder.Result() -// defer result.Body.Close() -// b, err := ioutil.ReadAll(result.Body) -// assert.NoError(suite.T(), err) -// assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b)) -// } + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), `{"error":"Forbidden: you do not have permission to fave this status"}`, string(b)) +} func TestStatusFaveTestSuite(t *testing.T) { suite.Run(t, new(StatusFaveTestSuite)) diff --git a/internal/api/client/statuses/statusmute_test.go b/internal/api/client/statuses/statusmute_test.go index 2fb94443a..62be671fa 100644 --- a/internal/api/client/statuses/statusmute_test.go +++ b/internal/api/client/statuses/statusmute_test.go @@ -151,19 +151,22 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -236,19 +239,22 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } diff --git a/internal/api/wellknown/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go index 84562187d..ce9bc0ccf 100644 --- a/internal/api/wellknown/webfinger/webfingerget_test.go +++ b/internal/api/wellknown/webfinger/webfingerget_test.go @@ -35,6 +35,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger" "github.com/superseriousbusiness/gotosocial/internal/cleaner" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" + "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/testrig" @@ -85,7 +87,19 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom config.SetAccountDomain(accountDomain) testrig.StopWorkers(&suite.state) testrig.StartNoopWorkers(&suite.state) - suite.processor = processing.NewProcessor(cleaner.New(&suite.state), suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(&suite.state), &suite.state, suite.emailSender) + + suite.processor = processing.NewProcessor( + cleaner.New(&suite.state), + suite.tc, + suite.federator, + testrig.NewTestOauthServer(suite.db), + testrig.NewTestMediaManager(&suite.state), + &suite.state, + suite.emailSender, + visibility.NewFilter(&suite.state), + interaction.NewFilter(&suite.state), + ) + suite.webfingerModule = webfinger.New(suite.processor) testrig.StartNoopWorkers(&suite.state) diff --git a/internal/federation/dereferencing/announce.go b/internal/federation/dereferencing/announce.go index d786d0695..a3eaf199d 100644 --- a/internal/federation/dereferencing/announce.go +++ b/internal/federation/dereferencing/announce.go @@ -69,12 +69,6 @@ func (d *Dereferencer) EnrichAnnounce( return nil, err } - // Generate an ID for the boost wrapper status. - boost.ID, err = id.NewULIDFromTime(boost.CreatedAt) - if err != nil { - return nil, gtserror.Newf("error generating id: %w", err) - } - // Set boost_of_uri again in case the // original URI was an indirect link. boost.BoostOfURI = target.URI @@ -92,6 +86,24 @@ func (d *Dereferencer) EnrichAnnounce( boost.Visibility = target.Visibility boost.Federated = target.Federated + // Ensure this Announce is permitted by the Announcee. + permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost) + if err != nil { + return nil, gtserror.Newf("error checking permitted status %s: %w", boost.URI, err) + } + + if !permit { + // Return a checkable error type that can be ignored. + err := gtserror.Newf("dropping unpermitted status: %s", boost.URI) + return nil, gtserror.SetNotPermitted(err) + } + + // Generate an ID for the boost wrapper status. + boost.ID, err = id.NewULIDFromTime(boost.CreatedAt) + if err != nil { + return nil, gtserror.Newf("error generating id: %w", err) + } + // Store the boost wrapper status in database. switch err = d.state.DB.PutStatus(ctx, boost); { case err == nil: diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go index bcc145c27..3bff0d1a2 100644 --- a/internal/federation/dereferencing/dereferencer.go +++ b/internal/federation/dereferencing/dereferencer.go @@ -22,6 +22,7 @@ import ( "sync" "time" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -83,7 +84,8 @@ type Dereferencer struct { converter *typeutils.Converter transportController transport.Controller mediaManager *media.Manager - visibility *visibility.Filter + visFilter *visibility.Filter + intFilter *interaction.Filter // in-progress dereferencing media / emoji derefMedia map[string]*media.ProcessingMedia @@ -102,12 +104,14 @@ type Dereferencer struct { handshakesMu sync.Mutex } -// NewDereferencer returns a Dereferencer initialized with the given parameters. +// NewDereferencer returns a Dereferencer +// initialized with the given parameters. func NewDereferencer( state *state.State, converter *typeutils.Converter, transportController transport.Controller, visFilter *visibility.Filter, + intFilter *interaction.Filter, mediaManager *media.Manager, ) Dereferencer { return Dereferencer{ @@ -115,7 +119,8 @@ func NewDereferencer( converter: converter, transportController: transportController, mediaManager: mediaManager, - visibility: visFilter, + visFilter: visFilter, + intFilter: intFilter, derefMedia: make(map[string]*media.ProcessingMedia), derefEmojis: make(map[string]*media.ProcessingEmoji), handshakes: make(map[string][]*url.URL), diff --git a/internal/federation/dereferencing/dereferencer_test.go b/internal/federation/dereferencing/dereferencer_test.go index 293118167..f00e876ae 100644 --- a/internal/federation/dereferencing/dereferencer_test.go +++ b/internal/federation/dereferencing/dereferencer_test.go @@ -22,6 +22,7 @@ import ( "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -79,8 +80,19 @@ func (suite *DereferencerStandardTestSuite) SetupTest() { suite.state.Storage = suite.storage visFilter := visibility.NewFilter(&suite.state) + intFilter := interaction.NewFilter(&suite.state) media := testrig.NewTestMediaManager(&suite.state) - suite.dereferencer = dereferencing.NewDereferencer(&suite.state, converter, testrig.NewTestTransportController(&suite.state, suite.client), visFilter, media) + suite.dereferencer = dereferencing.NewDereferencer( + &suite.state, + converter, + testrig.NewTestTransportController( + &suite.state, + suite.client, + ), + visFilter, + intFilter, + media, + ) testrig.StandardDBSetup(suite.db, nil) } diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index 0e227a0c1..88746fc3a 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -502,7 +502,8 @@ func (d *Dereferencer) enrichStatus( latestStatus.Local = status.Local // Check if this is a permitted status we should accept. - permit, err := d.isPermittedStatus(ctx, status, latestStatus) + // Function also sets "PendingApproval" bool as necessary. + permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus) if err != nil { return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err) } @@ -560,86 +561,6 @@ func (d *Dereferencer) enrichStatus( return latestStatus, apubStatus, nil } -// isPermittedStatus returns whether the given status -// is permitted to be stored on this instance, checking -// whether the author is suspended, and passes visibility -// checks against status being replied-to (if any). -func (d *Dereferencer) isPermittedStatus( - ctx context.Context, - existing *gtsmodel.Status, - status *gtsmodel.Status, -) ( - permitted bool, // is permitted? - err error, -) { - - // our failure condition handling - // at the end of this function for - // the case of permission = false. - onFail := func() (bool, error) { - if existing != nil { - log.Infof(ctx, "deleting unpermitted: %s", existing.URI) - - // Delete existing status from database as it's no longer permitted. - if err := d.state.DB.DeleteStatusByID(ctx, existing.ID); err != nil { - log.Errorf(ctx, "error deleting %s after permissivity fail: %v", existing.URI, err) - } - } - return false, nil - } - - if !status.Account.SuspendedAt.IsZero() { - // The status author is suspended, - // this shouldn't have reached here - // but it's a fast check anyways. - return onFail() - } - - if status.InReplyToURI == "" { - // This status isn't in - // reply to anything! - return true, nil - } - - if status.InReplyTo == nil { - // If no inReplyTo has been set, - // we return here for now as we - // can't perform further checks. - // - // Worst case we allow something - // through, and later on during - // refetch it will get deleted. - return true, nil - } - - if status.InReplyTo.BoostOfID != "" { - // We do not permit replies to - // boost wrapper statuses. (this - // shouldn't be able to happen). - return onFail() - } - - // Default to true - permitted = true - - if *status.InReplyTo.Local { - // Check visibility of inReplyTo to status author. - permitted, err = d.visibility.StatusVisible(ctx, - status.Account, - status.InReplyTo, - ) - if err != nil { - return false, gtserror.Newf("error checking in-reply-to visibility: %w", err) - } - } - - if permitted { - return true, nil - } - - return onFail() -} - func (d *Dereferencer) fetchStatusMentions( ctx context.Context, requestUser string, diff --git a/internal/federation/dereferencing/status_permitted.go b/internal/federation/dereferencing/status_permitted.go new file mode 100644 index 000000000..5c16f9f15 --- /dev/null +++ b/internal/federation/dereferencing/status_permitted.go @@ -0,0 +1,216 @@ +// 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 . + +package dereferencing + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +// isPermittedStatus returns whether the given status +// is permitted to be stored on this instance, checking: +// +// - author is not suspended +// - status passes visibility checks +// - status passes interaction policy checks +// +// If status is not permitted to be stored, the function +// will clean up after itself by removing the status. +// +// If status is a reply or a boost, and the author of +// the given status is only permitted to reply or boost +// pending approval, then "PendingApproval" will be set +// to "true" on status. Callers should check this +// and handle it as appropriate. +func (d *Dereferencer) isPermittedStatus( + ctx context.Context, + requestUser string, + existing *gtsmodel.Status, + status *gtsmodel.Status, +) ( + bool, // is permitted? + error, +) { + // our failure condition handling + // at the end of this function for + // the case of permission = false. + onFalse := func() (bool, error) { + if existing != nil { + log.Infof(ctx, "deleting unpermitted: %s", existing.URI) + + // Delete existing status from database as it's no longer permitted. + if err := d.state.DB.DeleteStatusByID(ctx, existing.ID); err != nil { + log.Errorf(ctx, "error deleting %s after permissivity fail: %v", existing.URI, err) + } + } + return false, nil + } + + if status.Account.IsSuspended() { + // The status author is suspended, + // this shouldn't have reached here + // but it's a fast check anyways. + log.Debugf(ctx, + "status author %s is suspended", + status.AccountURI, + ) + return onFalse() + } + + if inReplyTo := status.InReplyTo; inReplyTo != nil { + return d.isPermittedReply( + ctx, + requestUser, + status, + inReplyTo, + onFalse, + ) + } else if boostOf := status.BoostOf; boostOf != nil { + return d.isPermittedBoost( + ctx, + requestUser, + status, + boostOf, + onFalse, + ) + } + + // Nothing else stopping this. + return true, nil +} + +func (d *Dereferencer) isPermittedReply( + ctx context.Context, + requestUser string, + status *gtsmodel.Status, + inReplyTo *gtsmodel.Status, + onFalse func() (bool, error), +) (bool, error) { + if inReplyTo.BoostOfID != "" { + // We do not permit replies to + // boost wrapper statuses. (this + // shouldn't be able to happen). + log.Info(ctx, "rejecting reply to boost wrapper status") + return onFalse() + } + + // Check visibility of local + // inReplyTo to replying account. + if inReplyTo.IsLocal() { + visible, err := d.visFilter.StatusVisible(ctx, + status.Account, + inReplyTo, + ) + if err != nil { + err := gtserror.Newf("error checking inReplyTo visibility: %w", err) + return false, err + } + + // Our status is not visible to the + // account trying to do the reply. + if !visible { + return onFalse() + } + } + + // Check interaction policy of inReplyTo. + replyable, err := d.intFilter.StatusReplyable(ctx, + status.Account, + inReplyTo, + ) + if err != nil { + err := gtserror.Newf("error checking status replyability: %w", err) + return false, err + } + + if replyable.Forbidden() { + // Replier is not permitted + // to do this interaction. + return onFalse() + } + + // TODO in next PR: check conditional / + // with approval and deref Accept. + if !replyable.Permitted() { + return onFalse() + } + + return true, nil +} + +func (d *Dereferencer) isPermittedBoost( + ctx context.Context, + requestUser string, + status *gtsmodel.Status, + boostOf *gtsmodel.Status, + onFalse func() (bool, error), +) (bool, error) { + if boostOf.BoostOfID != "" { + // We do not permit boosts of + // boost wrapper statuses. (this + // shouldn't be able to happen). + log.Info(ctx, "rejecting boost of boost wrapper status") + return onFalse() + } + + // Check visibility of local + // boostOf to boosting account. + if boostOf.IsLocal() { + visible, err := d.visFilter.StatusVisible(ctx, + status.Account, + boostOf, + ) + if err != nil { + err := gtserror.Newf("error checking boostOf visibility: %w", err) + return false, err + } + + // Our status is not visible to the + // account trying to do the boost. + if !visible { + return onFalse() + } + } + + // Check interaction policy of boostOf. + boostable, err := d.intFilter.StatusBoostable(ctx, + status.Account, + boostOf, + ) + if err != nil { + err := gtserror.Newf("error checking status boostability: %w", err) + return false, err + } + + if boostable.Forbidden() { + // Booster is not permitted + // to do this interaction. + return onFalse() + } + + // TODO in next PR: check conditional / + // with approval and deref Accept. + if !boostable.Permitted() { + return onFalse() + } + + return true, nil +} diff --git a/internal/federation/federatingactor_test.go b/internal/federation/federatingactor_test.go index b5b65827b..af12b409a 100644 --- a/internal/federation/federatingactor_test.go +++ b/internal/federation/federatingactor_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" @@ -68,6 +69,7 @@ func (suite *FederatingActorTestSuite) TestSendNoRemoteFollowers() { tc, suite.typeconverter, visibility.NewFilter(&suite.state), + interaction.NewFilter(&suite.state), testrig.NewTestMediaManager(&suite.state), ) @@ -122,6 +124,7 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() { tc, suite.typeconverter, visibility.NewFilter(&suite.state), + interaction.NewFilter(&suite.state), testrig.NewTestMediaManager(&suite.state), ) diff --git a/internal/federation/federator.go b/internal/federation/federator.go index f97d73cf8..4e11c7d4d 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -22,6 +22,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -52,6 +53,7 @@ func NewFederator( transportController transport.Controller, converter *typeutils.Converter, visFilter *visibility.Filter, + intFilter *interaction.Filter, mediaManager *media.Manager, ) *Federator { clock := &Clock{} @@ -62,7 +64,14 @@ func NewFederator( converter: converter, transportController: transportController, mediaManager: mediaManager, - Dereferencer: dereferencing.NewDereferencer(state, converter, transportController, visFilter, mediaManager), + Dereferencer: dereferencing.NewDereferencer( + state, + converter, + transportController, + visFilter, + intFilter, + mediaManager, + ), } actor := newFederatingActor(f, f, federatingDB, clock) f.actor = actor diff --git a/internal/filter/interaction/filter.go b/internal/filter/interaction/filter.go new file mode 100644 index 000000000..49e0758c1 --- /dev/null +++ b/internal/filter/interaction/filter.go @@ -0,0 +1,34 @@ +// 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 . + +package interaction + +import ( + "github.com/superseriousbusiness/gotosocial/internal/state" +) + +// Filter packages up logic for checking whether +// an interaction is permitted within set policies. +type Filter struct { + state *state.State +} + +// NewFilter returns a new Filter +// that will use the provided state. +func NewFilter(state *state.State) *Filter { + return &Filter{state: state} +} diff --git a/internal/filter/interaction/interactable.go b/internal/filter/interaction/interactable.go new file mode 100644 index 000000000..fe31ce8f2 --- /dev/null +++ b/internal/filter/interaction/interactable.go @@ -0,0 +1,561 @@ +// 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 . + +package interaction + +import ( + "context" + "fmt" + "slices" + + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +type matchType int + +const ( + none matchType = 0 + implicit matchType = 1 + explicit matchType = 2 +) + +// startedThread returns true if requester started +// the thread that the given status is part of. +// Ie., requester created the first post in the thread. +func (f *Filter) startedThread( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) (bool, error) { + parents, err := f.state.DB.GetStatusParents(ctx, status) + if err != nil { + return false, fmt.Errorf("db error getting parents of %s: %w", status.ID, err) + } + + if len(parents) == 0 { + // No parents available. Just check + // if this status belongs to requester. + return status.AccountID == requester.ID, nil + } + + // Check if OG status owned by requester. + return parents[0].AccountID == requester.ID, nil +} + +// StatusLikeable checks if the given status +// is likeable by the requester account. +// +// Callers to this function should have already +// checked the visibility of status to requester, +// including taking account of blocks, as this +// function does not do visibility checks, only +// interaction policy checks. +func (f *Filter) StatusLikeable( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) (*gtsmodel.PolicyCheckResult, error) { + if requester.ID == status.AccountID { + // Status author themself can + // always like their own status, + // no need for further checks. + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueAuthor), + }, nil + } + + switch { + // If status has policy set, check against that. + case status.InteractionPolicy != nil: + return f.checkPolicy( + ctx, + requester, + status, + status.InteractionPolicy.CanLike, + ) + + // If status is local and has no policy set, + // check against the default policy for this + // visibility, as we're interaction-policy aware. + case *status.Local: + policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) + return f.checkPolicy( + ctx, + requester, + status, + policy.CanLike, + ) + + // Otherwise, assume the status is from an + // instance that does not use / does not care + // about interaction policies, and just return OK. + default: + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + }, nil + } +} + +// StatusReplyable checks if the given status +// is replyable by the requester account. +// +// Callers to this function should have already +// checked the visibility of status to requester, +// including taking account of blocks, as this +// function does not do visibility checks, only +// interaction policy checks. +func (f *Filter) StatusReplyable( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) (*gtsmodel.PolicyCheckResult, error) { + if util.PtrOrValue(status.PendingApproval, false) { + // Target status is pending approval, + // check who started this thread. + startedThread, err := f.startedThread( + ctx, + requester, + status, + ) + if err != nil { + err := gtserror.Newf("error checking thread ownership: %w", err) + return nil, err + } + + if !startedThread { + // If status is itself still pending approval, + // and the requester didn't start this thread, + // then buddy, any status that tries to reply + // to it must be pending approval too. We do + // this to prevent someone replying to a status + // with a policy set that causes that reply to + // require approval, *THEN* replying to their + // own reply (which may not have a policy set) + // and having the reply-to-their-own-reply go + // through as Permitted. None of that! + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionWithApproval, + }, nil + } + } + + if requester.ID == status.AccountID { + // Status author themself can + // always reply to their own status, + // no need for further checks. + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueAuthor), + }, nil + } + + // If requester is replied to by this status, + // then just return OK, it's functionally equivalent + // to them being mentioned, and easier to check! + if status.InReplyToAccountID == requester.ID { + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueMentioned), + }, nil + } + + // Check if requester mentioned by this status. + // + // Prefer checking by ID, fall back to URI, URL, + // or NameString for not-yet enriched statuses. + mentioned := slices.ContainsFunc( + status.Mentions, + func(m *gtsmodel.Mention) bool { + switch { + + // Check by ID - most accurate. + case m.TargetAccountID != "": + return m.TargetAccountID == requester.ID + + // Check by URI - also accurate. + case m.TargetAccountURI != "": + return m.TargetAccountURI == requester.URI + + // Check by URL - probably accurate. + case m.TargetAccountURL != "": + return m.TargetAccountURL == requester.URL + + // Fall back to checking by namestring. + case m.NameString != "": + username, host, err := util.ExtractNamestringParts(m.NameString) + if err != nil { + log.Debugf(ctx, "error checking if mentioned: %v", err) + return false + } + + if requester.IsLocal() { + // Local requester has empty string + // domain so check using config. + return username == requester.Username && + (host == config.GetHost() || host == config.GetAccountDomain()) + } + + // Remote requester has domain set. + return username == requester.Username && + host == requester.Domain + + default: + // Not mentioned. + return false + } + }, + ) + + if mentioned { + // A mentioned account can always + // reply, no need for further checks. + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueMentioned), + }, nil + } + + switch { + // If status has policy set, check against that. + case status.InteractionPolicy != nil: + return f.checkPolicy( + ctx, + requester, + status, + status.InteractionPolicy.CanReply, + ) + + // If status is local and has no policy set, + // check against the default policy for this + // visibility, as we're interaction-policy aware. + case *status.Local: + policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) + return f.checkPolicy( + ctx, + requester, + status, + policy.CanReply, + ) + + // Otherwise, assume the status is from an + // instance that does not use / does not care + // about interaction policies, and just return OK. + default: + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + }, nil + } +} + +// StatusBoostable checks if the given status +// is boostable by the requester account. +// +// Callers to this function should have already +// checked the visibility of status to requester, +// including taking account of blocks, as this +// function does not do visibility checks, only +// interaction policy checks. +func (f *Filter) StatusBoostable( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) (*gtsmodel.PolicyCheckResult, error) { + if status.Visibility == gtsmodel.VisibilityDirect { + log.Trace(ctx, "direct statuses are not boostable") + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionForbidden, + }, nil + } + + if requester.ID == status.AccountID { + // Status author themself can + // always boost non-directs, + // no need for further checks. + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueAuthor), + }, nil + } + + switch { + // If status has policy set, check against that. + case status.InteractionPolicy != nil: + return f.checkPolicy( + ctx, + requester, + status, + status.InteractionPolicy.CanAnnounce, + ) + + // If status is local and has no policy set, + // check against the default policy for this + // visibility, as we're interaction-policy aware. + case *status.Local: + policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) + return f.checkPolicy( + ctx, + requester, + status, + policy.CanAnnounce, + ) + + // Otherwise, assume the status is from an + // instance that does not use / does not care + // about interaction policies, and just return OK. + default: + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + }, nil + } +} + +func (f *Filter) checkPolicy( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, + rules gtsmodel.PolicyRules, +) (*gtsmodel.PolicyCheckResult, error) { + + // Wrap context to be able to + // cache some database calls. + fctx := new(filterctx) + fctx.Context = ctx + + // Check if requester matches a PolicyValue + // to be always allowed to do this. + matchAlways, matchAlwaysValue, err := f.matchPolicy(fctx, + requester, + status, + rules.Always, + ) + if err != nil { + return nil, gtserror.Newf("error checking policy match: %w", err) + } + + // Check if requester matches a PolicyValue + // to be allowed to do this pending approval. + matchWithApproval, _, err := f.matchPolicy(fctx, + requester, + status, + rules.WithApproval, + ) + if err != nil { + return nil, gtserror.Newf("error checking policy approval match: %w", err) + } + + switch { + + // Prefer explicit match, + // prioritizing "always". + case matchAlways == explicit: + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + PermittedMatchedOn: &matchAlwaysValue, + }, nil + + case matchWithApproval == explicit: + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionWithApproval, + }, nil + + // Then try implicit match, + // prioritizing "always". + case matchAlways == implicit: + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionPermitted, + PermittedMatchedOn: &matchAlwaysValue, + }, nil + + case matchWithApproval == implicit: + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionWithApproval, + }, nil + } + + // No match. + return >smodel.PolicyCheckResult{ + Permission: gtsmodel.PolicyPermissionForbidden, + }, nil +} + +// matchPolicy returns whether requesting account +// matches any of the policy values for given status, +// returning the policy it matches on and match type. +// uses a *filterctx to cache certain db results. +func (f *Filter) matchPolicy( + ctx *filterctx, + requester *gtsmodel.Account, + status *gtsmodel.Status, + policyValues []gtsmodel.PolicyValue, +) ( + matchType, + gtsmodel.PolicyValue, + error, +) { + var ( + match = none + value gtsmodel.PolicyValue + ) + + for _, p := range policyValues { + switch p { + + // Check if anyone + // can do this. + case gtsmodel.PolicyValuePublic: + match = implicit + value = gtsmodel.PolicyValuePublic + + // Check if follower + // of status owner. + case gtsmodel.PolicyValueFollowers: + inFollowers, err := f.inFollowers(ctx, + requester, + status, + ) + if err != nil { + return 0, "", err + } + if inFollowers { + match = implicit + value = gtsmodel.PolicyValueFollowers + } + + // Check if followed + // by status owner. + case gtsmodel.PolicyValueFollowing: + inFollowing, err := f.inFollowing(ctx, + requester, + status, + ) + if err != nil { + return 0, "", err + } + if inFollowing { + match = implicit + value = gtsmodel.PolicyValueFollowing + } + + // Check if replied-to by or + // mentioned in the status. + case gtsmodel.PolicyValueMentioned: + if (status.InReplyToAccountID == requester.ID) || + status.MentionsAccount(requester.ID) { + // Return early as we've + // found an explicit match. + match = explicit + value = gtsmodel.PolicyValueMentioned + return match, value, nil + } + + // Check if PolicyValue specifies + // requester explicitly. + default: + if string(p) == requester.URI { + // Return early as we've + // found an explicit match. + match = explicit + value = gtsmodel.PolicyValue(requester.URI) + return match, value, nil + } + } + } + + // Return either "" or "implicit", + // and the policy value matched + // against (if set). + return match, value, nil +} + +// inFollowers returns whether requesting account is following +// status author, uses *filterctx type for db result caching. +func (f *Filter) inFollowers( + ctx *filterctx, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) ( + bool, + error, +) { + if ctx.inFollowersOnce == 0 { + var err error + + // Load the 'inFollowers' result from database. + ctx.inFollowers, err = f.state.DB.IsFollowing(ctx, + requester.ID, + status.AccountID, + ) + if err != nil { + return false, gtserror.Newf("error checking follow status: %w", err) + } + + // Mark value as stored. + ctx.inFollowersOnce = 1 + } + + // Return stored value. + return ctx.inFollowers, nil +} + +// inFollowing returns whether status author is following +// requesting account, uses *filterctx for db result caching. +func (f *Filter) inFollowing( + ctx *filterctx, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) ( + bool, + error, +) { + if ctx.inFollowingOnce == 0 { + var err error + + // Load the 'inFollowers' result from database. + ctx.inFollowing, err = f.state.DB.IsFollowing(ctx, + status.AccountID, + requester.ID, + ) + if err != nil { + return false, gtserror.Newf("error checking follow status: %w", err) + } + + // Mark value as stored. + ctx.inFollowingOnce = 1 + } + + // Return stored value. + return ctx.inFollowing, nil +} + +// filterctx wraps a context.Context to also +// store loadable data relevant to a fillter +// operation from the database, such that it +// only needs to be loaded once IF required. +type filterctx struct { + context.Context + + inFollowers bool + inFollowersOnce int32 + + inFollowing bool + inFollowingOnce int32 +} diff --git a/internal/filter/visibility/boostable.go b/internal/filter/visibility/boostable.go deleted file mode 100644 index 7362ad45c..000000000 --- a/internal/filter/visibility/boostable.go +++ /dev/null @@ -1,57 +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 . - -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 - } - - return true, nil -} diff --git a/internal/filter/visibility/boostable_test.go b/internal/filter/visibility/boostable_test.go deleted file mode 100644 index fd29e7305..000000000 --- a/internal/filter/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 . - -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/filter/visibility/status.go b/internal/filter/visibility/status.go index 5e2052ae4..be1c6a350 100644 --- a/internal/filter/visibility/status.go +++ b/internal/filter/visibility/status.go @@ -25,6 +25,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // StatusesVisible calls StatusVisible for each status in the statuses slice, and returns a slice of only statuses which are visible to the requester. @@ -41,8 +42,15 @@ func (f *Filter) StatusesVisible(ctx context.Context, requester *gtsmodel.Accoun 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) { +// StatusVisible will check if status is visible to requester, +// accounting for requester with no auth (i.e is nil), suspensions, +// disabled local users, pending approvals, account blocks, +// and status visibility settings. +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. @@ -75,8 +83,14 @@ func (f *Filter) StatusVisible(ctx context.Context, requester *gtsmodel.Account, 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) { +// 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) @@ -90,6 +104,14 @@ func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Accoun return false, nil } + if util.PtrOrValue(status.PendingApproval, false) { + // Use a different visibility heuristic + // for pending approval statuses. + return f.isPendingStatusVisible(ctx, + requester, status, + ) + } + if status.Visibility == gtsmodel.VisibilityPublic { // This status will be visible to all. return true, nil @@ -176,6 +198,41 @@ func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Accoun } } +func (f *Filter) isPendingStatusVisible( + _ context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) (bool, error) { + if requester == nil { + // Any old tom, dick, and harry can't + // see pending-approval statuses, + // no matter what their visibility. + return false, nil + } + + if status.AccountID == requester.ID { + // This is requester's status, + // so they can always see it. + return true, nil + } + + if status.InReplyToAccountID == requester.ID { + // This status replies to requester, + // so they can always see it (else + // they can't approve it). + return true, nil + } + + if status.BoostOfAccountID == requester.ID { + // This status boosts requester, + // so they can always see it. + return true, nil + } + + // Nobody else can see this. + 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. diff --git a/internal/filter/visibility/status_test.go b/internal/filter/visibility/status_test.go index ad6bc66df..6f8bb12b4 100644 --- a/internal/filter/visibility/status_test.go +++ b/internal/filter/visibility/status_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" ) type StatusVisibleTestSuite struct { @@ -156,6 +157,49 @@ func (suite *StatusVisibleTestSuite) TestStatusNotVisibleIfNotFollowingCached() suite.False(visible) } +func (suite *StatusVisibleTestSuite) TestVisiblePending() { + ctx := context.Background() + + // Copy the test status and mark + // the copy as pending approval. + // + // This is a status from admin + // that replies to zork. + testStatus := new(gtsmodel.Status) + *testStatus = *suite.testStatuses["admin_account_status_3"] + testStatus.PendingApproval = util.Ptr(true) + + for _, testCase := range []struct { + acct *gtsmodel.Account + visible bool + }{ + { + acct: suite.testAccounts["admin_account"], + visible: true, // Own status, always visible. + }, + { + acct: suite.testAccounts["local_account_1"], + visible: true, // Reply to zork, always visible. + }, + { + acct: suite.testAccounts["local_account_2"], + visible: false, // None of their business. + }, + { + acct: suite.testAccounts["remote_account_1"], + visible: false, // None of their business. + }, + { + acct: nil, // Unauthed request. + visible: false, // None of their business. + }, + } { + visible, err := suite.filter.StatusVisible(ctx, testCase.acct, testStatus) + suite.NoError(err) + suite.Equal(testCase.visible, visible) + } +} + func TestStatusVisibleTestSuite(t *testing.T) { suite.Run(t, new(StatusVisibleTestSuite)) } diff --git a/internal/gtsmodel/interactionpolicy.go b/internal/gtsmodel/interactionpolicy.go index 993763dc3..d8d890e69 100644 --- a/internal/gtsmodel/interactionpolicy.go +++ b/internal/gtsmodel/interactionpolicy.go @@ -111,26 +111,74 @@ func (p PolicyValue) FeasibleForVisibility(v Visibility) bool { type PolicyValues []PolicyValue -// PolicyResult represents the result of -// checking an Actor URI and interaction -// type against the conditions of an -// InteractionPolicy to determine if that -// interaction is permitted. -type PolicyResult int +// PolicyPermission represents the permission +// state for a certain Actor URI and interaction +// type, in relation to a policy. +type PolicyPermission int const ( // Interaction is forbidden for this // PolicyValue + interaction combination. - PolicyResultForbidden PolicyResult = iota + PolicyPermissionForbidden PolicyPermission = iota // Interaction is conditionally permitted // for this PolicyValue + interaction combo, // pending approval by the item owner. - PolicyResultWithApproval + PolicyPermissionWithApproval // Interaction is permitted for this // PolicyValue + interaction combination. - PolicyResultPermitted + PolicyPermissionPermitted ) +// PolicyCheckResult encapsulates the results +// of checking a certain Actor URI + type +// of interaction against an interaction policy. +type PolicyCheckResult struct { + // Permission permitted / + // with approval / forbidden. + Permission PolicyPermission + + // Value that this check matched on. + // Only set if Permission = permitted. + PermittedMatchedOn *PolicyValue +} + +// MatchedOnCollection returns true if this policy check +// result turned up Permitted, and matched based on the +// requester's presence in a followers or following collection. +func (pcr *PolicyCheckResult) MatchedOnCollection() bool { + if !pcr.Permitted() { + // Not permitted at all + // so definitely didn't + // match on collection. + return false + } + + if pcr.PermittedMatchedOn == nil { + return false + } + + return *pcr.PermittedMatchedOn == PolicyValueFollowers || + *pcr.PermittedMatchedOn == PolicyValueFollowing +} + +// Permitted returns true if this policy +// check resulted in Permission = permitted. +func (pcr *PolicyCheckResult) Permitted() bool { + return pcr.Permission == PolicyPermissionPermitted +} + +// Permitted returns true if this policy +// check resulted in Permission = with approval. +func (pcr *PolicyCheckResult) WithApproval() bool { + return pcr.Permission == PolicyPermissionWithApproval +} + +// Permitted returns true if this policy +// check resulted in Permission = forbidden. +func (pcr *PolicyCheckResult) Forbidden() bool { + return pcr.Permission == PolicyPermissionForbidden +} + // An InteractionPolicy determines which // interactions will be accepted for an // item, and according to what rules. diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index 65bb40292..d65d7360c 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -38,7 +38,7 @@ type Processor struct { state *state.State converter *typeutils.Converter mediaManager *media.Manager - filter *visibility.Filter + visFilter *visibility.Filter formatter *text.Formatter federator *federation.Federator parseMention gtsmodel.ParseMentionFunc @@ -52,7 +52,7 @@ func New( converter *typeutils.Converter, mediaManager *media.Manager, federator *federation.Federator, - filter *visibility.Filter, + visFilter *visibility.Filter, parseMention gtsmodel.ParseMentionFunc, ) Processor { return Processor{ @@ -60,7 +60,7 @@ func New( state: state, converter: converter, mediaManager: mediaManager, - filter: filter, + visFilter: visFilter, formatter: text.NewFormatter(state.DB), federator: federator, parseMention: parseMention, diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go index b9ecf0217..d64108d3a 100644 --- a/internal/processing/account/bookmarks.go +++ b/internal/processing/account/bookmarks.go @@ -64,7 +64,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode return nil, gtserror.NewErrorInternalError(err) // A real error has occurred. } - visible, err := p.filter.StatusVisible(ctx, requestingAccount, status) + visible, err := p.visFilter.StatusVisible(ctx, requestingAccount, status) if err != nil { log.Errorf(ctx, "error checking bookmarked status visibility: %s", err) continue diff --git a/internal/processing/account/lists.go b/internal/processing/account/lists.go index 12fbb884b..1d92bee82 100644 --- a/internal/processing/account/lists.go +++ b/internal/processing/account/lists.go @@ -42,7 +42,7 @@ func (p *Processor) ListsGet(ctx context.Context, requestingAccount *gtsmodel.Ac return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err)) } - visible, err := p.filter.AccountVisible(ctx, requestingAccount, targetAccount) + visible, err := p.visFilter.AccountVisible(ctx, requestingAccount, targetAccount) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err)) } diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go index 593c30e27..2bab812e3 100644 --- a/internal/processing/account/statuses.go +++ b/internal/processing/account/statuses.go @@ -92,7 +92,7 @@ func (p *Processor) StatusesGet( // Filtering + serialization process is the same for // both pinned status queries and 'normal' ones. - filtered, err := p.filter.StatusesVisible(ctx, requestingAccount, statuses) + filtered, err := p.visFilter.StatusesVisible(ctx, requestingAccount, statuses) if err != nil { return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/admin/admin_test.go b/internal/processing/admin/admin_test.go index 97b055158..3251264b6 100644 --- a/internal/processing/admin/admin_test.go +++ b/internal/processing/admin/admin_test.go @@ -23,6 +23,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" @@ -114,6 +115,8 @@ func (suite *AdminStandardTestSuite) SetupTest() { suite.mediaManager, &suite.state, suite.emailSender, + visibility.NewFilter(&suite.state), + interaction.NewFilter(&suite.state), ) testrig.StartWorkers(&suite.state, suite.processor.Workers()) diff --git a/internal/processing/common/account.go b/internal/processing/common/account.go index c0daf647d..05e974513 100644 --- a/internal/processing/common/account.go +++ b/internal/processing/common/account.go @@ -55,7 +55,7 @@ func (p *Processor) GetTargetAccountBy( } // Check whether target account is visible to requesting account. - visible, err = p.filter.AccountVisible(ctx, requester, target) + visible, err = p.visFilter.AccountVisible(ctx, requester, target) if err != nil { return nil, false, gtserror.NewErrorInternalError(err) } @@ -241,7 +241,7 @@ func (p *Processor) getVisibleAPIAccounts( } // Check whether this account is visible to requesting account. - visible, err := p.filter.AccountVisible(ctx, requester, account) + visible, err := p.visFilter.AccountVisible(ctx, requester, account) if err != nil { l.Errorf("error checking account visibility: %v", err) continue diff --git a/internal/processing/common/common.go b/internal/processing/common/common.go index 942cecc59..29def3506 100644 --- a/internal/processing/common/common.go +++ b/internal/processing/common/common.go @@ -33,7 +33,7 @@ type Processor struct { media *media.Manager converter *typeutils.Converter federator *federation.Federator - filter *visibility.Filter + visFilter *visibility.Filter } // New returns a new Processor instance. @@ -42,13 +42,13 @@ func New( media *media.Manager, converter *typeutils.Converter, federator *federation.Federator, - filter *visibility.Filter, + visFilter *visibility.Filter, ) Processor { return Processor{ state: state, media: media, converter: converter, federator: federator, - filter: filter, + visFilter: visFilter, } } diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go index cce1967b9..3ef643292 100644 --- a/internal/processing/common/status.go +++ b/internal/processing/common/status.go @@ -63,7 +63,7 @@ func (p *Processor) GetTargetStatusBy( } // Check whether target status is visible to requesting account. - visible, err = p.filter.StatusVisible(ctx, requester, target) + visible, err = p.visFilter.StatusVisible(ctx, requester, target) if err != nil { return nil, false, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/fedi/fedi.go b/internal/processing/fedi/fedi.go index b08f0eefd..52a9d70bf 100644 --- a/internal/processing/fedi/fedi.go +++ b/internal/processing/fedi/fedi.go @@ -32,7 +32,7 @@ type Processor struct { state *state.State federator *federation.Federator converter *typeutils.Converter - filter *visibility.Filter + visFilter *visibility.Filter } // New returns a @@ -42,13 +42,13 @@ func New( common *common.Processor, converter *typeutils.Converter, federator *federation.Federator, - filter *visibility.Filter, + visFilter *visibility.Filter, ) Processor { return Processor{ c: common, state: state, federator: federator, converter: converter, - filter: filter, + visFilter: visFilter, } } diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go index 7c4d4beec..fe07b5a95 100644 --- a/internal/processing/fedi/status.go +++ b/internal/processing/fedi/status.go @@ -68,7 +68,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI return nil, gtserror.NewErrorNotFound(errors.New(text)) } - visible, err := p.filter.StatusVisible(ctx, requestingAcct, status) + visible, err := p.visFilter.StatusVisible(ctx, requestingAcct, status) if err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -163,7 +163,7 @@ func (p *Processor) StatusRepliesGet( } // Reslice replies dropping all those invisible to requester. - replies, err = p.filter.StatusesVisible(ctx, requestingAcct, replies) + replies, err = p.visFilter.StatusesVisible(ctx, requestingAcct, replies) if err != nil { err := gtserror.Newf("error filtering status replies: %w", err) return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/processor.go b/internal/processing/processor.go index a07df76e1..5afcf0721 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -21,6 +21,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/cleaner" "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" mm "github.com/superseriousbusiness/gotosocial/internal/media" @@ -173,11 +174,10 @@ func NewProcessor( mediaManager *mm.Manager, state *state.State, emailSender email.Sender, + visFilter *visibility.Filter, + intFilter *interaction.Filter, ) *Processor { - var ( - parseMentionFunc = GetParseMentionFunc(state, federator) - filter = visibility.NewFilter(state) - ) + var parseMentionFunc = GetParseMentionFunc(state, federator) processor := &Processor{ converter: converter, @@ -191,26 +191,26 @@ func NewProcessor( // // Start with sub processors that will // be required by the workers processor. - common := common.New(state, mediaManager, converter, federator, filter) - processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc) + common := common.New(state, mediaManager, converter, federator, visFilter) + processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc) processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController()) processor.stream = stream.New(state, oauthServer) // Instantiate the rest of the sub // processors + pin them to this struct. - processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc) + processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc) processor.admin = admin.New(&common, state, cleaner, federator, converter, mediaManager, federator.TransportController(), emailSender) - processor.conversations = conversations.New(state, converter, filter) - processor.fedi = fedi.New(state, &common, converter, federator, filter) + processor.conversations = conversations.New(state, converter, visFilter) + processor.fedi = fedi.New(state, &common, converter, federator, visFilter) processor.filtersv1 = filtersv1.New(state, converter, &processor.stream) processor.filtersv2 = filtersv2.New(state, converter, &processor.stream) processor.list = list.New(state, converter) processor.markers = markers.New(state, converter) processor.polls = polls.New(&common, state, converter) processor.report = report.New(state, converter) - processor.timeline = timeline.New(state, converter, filter) - processor.search = search.New(state, federator, converter, filter) - processor.status = status.New(state, &common, &processor.polls, federator, converter, filter, parseMentionFunc) + processor.timeline = timeline.New(state, converter, visFilter) + processor.search = search.New(state, federator, converter, visFilter) + processor.status = status.New(state, &common, &processor.polls, federator, converter, visFilter, intFilter, parseMentionFunc) processor.user = user.New(state, converter, oauthServer, emailSender) // The advanced migrations processor sequences advanced migrations from all other processors. @@ -223,7 +223,7 @@ func NewProcessor( state, federator, converter, - filter, + visFilter, emailSender, &processor.account, &processor.media, diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index 767e8b5ef..d0898a98d 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -25,6 +25,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" @@ -122,7 +123,17 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.emailSender = testrig.NewEmailSender("../../web/template/", nil) - suite.processor = processing.NewProcessor(cleaner.New(&suite.state), suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, &suite.state, suite.emailSender) + suite.processor = processing.NewProcessor( + cleaner.New(&suite.state), + suite.typeconverter, + suite.federator, + suite.oauthServer, + suite.mediaManager, + &suite.state, + suite.emailSender, + visibility.NewFilter(&suite.state), + interaction.NewFilter(&suite.state), + ) testrig.StartWorkers(&suite.state, suite.processor.Workers()) testrig.StandardDBSetup(suite.db, suite.testAccounts) diff --git a/internal/processing/search/search.go b/internal/processing/search/search.go index 6c0ab2457..18008647c 100644 --- a/internal/processing/search/search.go +++ b/internal/processing/search/search.go @@ -28,15 +28,15 @@ type Processor struct { state *state.State federator *federation.Federator converter *typeutils.Converter - filter *visibility.Filter + visFilter *visibility.Filter } // New returns a new status processor. -func New(state *state.State, federator *federation.Federator, converter *typeutils.Converter, filter *visibility.Filter) Processor { +func New(state *state.State, federator *federation.Federator, converter *typeutils.Converter, visFilter *visibility.Filter) Processor { return Processor{ state: state, federator: federator, converter: converter, - filter: filter, + visFilter: visFilter, } } diff --git a/internal/processing/search/util.go b/internal/processing/search/util.go index 190289155..8043affd9 100644 --- a/internal/processing/search/util.go +++ b/internal/processing/search/util.go @@ -103,7 +103,7 @@ func (p *Processor) packageStatuses( for _, status := range statuses { // Ensure requester can see result status. - visible, err := p.filter.StatusVisible(ctx, requestingAccount, status) + visible, err := p.visFilter.StatusVisible(ctx, requestingAccount, status) if err != nil { err = gtserror.Newf("error checking visibility of status %s for account %s: %w", status.ID, requestingAccount.ID, err) return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go index 1b410bb0a..d6a0c2457 100644 --- a/internal/processing/status/boost.go +++ b/internal/processing/status/boost.go @@ -66,7 +66,7 @@ func (p *Processor) BoostCreate( } // Ensure valid boost target for requester. - boostable, err := p.filter.StatusBoostable(ctx, + policyResult, err := p.intFilter.StatusBoostable(ctx, requester, target, ) @@ -75,12 +75,14 @@ func (p *Processor) BoostCreate( return nil, gtserror.NewErrorInternalError(err) } - if !boostable { - err := gtserror.New("status is not boostable") - return nil, gtserror.NewErrorNotFound(err) + if policyResult.Forbidden() { + const errText = "you do not have permission to boost this status" + err := gtserror.New(errText) + return nil, gtserror.NewErrorForbidden(err, errText) } - // Status is visible and boostable. + // Status is visible and boostable + // (though maybe pending approval). boost, err := p.converter.StatusToBoost(ctx, target, requester, @@ -90,6 +92,29 @@ func (p *Processor) BoostCreate( return nil, gtserror.NewErrorInternalError(err) } + // Derive pendingApproval status. + var pendingApproval bool + switch { + case policyResult.WithApproval(): + // We're allowed to do + // this pending approval. + pendingApproval = true + + case policyResult.MatchedOnCollection(): + // We're permitted to do this, but since + // we matched due to presence in a followers + // or following collection, we should mark + // as pending approval and wait for an accept. + pendingApproval = true + + case policyResult.Permitted(): + // We're permitted to do this + // based on another kind of match. + pendingApproval = false + } + + boost.PendingApproval = &pendingApproval + // Store the new boost. if err := p.state.DB.PutStatus(ctx, boost); err != nil { return nil, gtserror.NewErrorInternalError(err) @@ -184,7 +209,7 @@ func (p *Processor) StatusBoostedBy(ctx context.Context, requestingAccount *gtsm targetStatus = boostedStatus } - visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus) + visible, err := p.visFilter.StatusVisible(ctx, requestingAccount, targetStatus) if err != nil { err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err) return nil, gtserror.NewErrorNotFound(err) diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go index 013cf4827..9f3a7d089 100644 --- a/internal/processing/status/context.go +++ b/internal/processing/status/context.go @@ -341,7 +341,7 @@ func (p *Processor) ContextGet( // Convert ancestors + filter // out ones that aren't visible. for _, status := range threadContext.ancestors { - if v, err := p.filter.StatusVisible(ctx, requester, status); err == nil && v { + if v, err := p.visFilter.StatusVisible(ctx, requester, status); err == nil && v { status, err := convert(ctx, status, requester) if err == nil { apiContext.Ancestors = append(apiContext.Ancestors, *status) @@ -352,7 +352,7 @@ func (p *Processor) ContextGet( // Convert descendants + filter // out ones that aren't visible. for _, status := range threadContext.descendants { - if v, err := p.filter.StatusVisible(ctx, requester, status); err == nil && v { + if v, err := p.visFilter.StatusVisible(ctx, requester, status); err == nil && v { status, err := convert(ctx, status, requester) if err == nil { apiContext.Descendants = append(apiContext.Descendants, *status) @@ -453,7 +453,7 @@ func (p *Processor) WebContextGet( // Ensure status is actually // visible to just anyone, and // hide / don't include it if not. - v, err := p.filter.StatusVisible(ctx, nil, status) + v, err := p.visFilter.StatusVisible(ctx, nil, status) if err != nil || !v { if !inReplies { // Main thread entry hidden. diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index e6381eb85..10e19ac43 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -169,6 +169,8 @@ func (p *Processor) Create( func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, inReplyToID string) gtserror.WithCode { if inReplyToID == "" { + // Not a reply. + // Nothing to do. return nil } @@ -191,6 +193,45 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac return errWithCode } + // Ensure valid reply target for requester. + policyResult, err := p.intFilter.StatusReplyable(ctx, + requester, + inReplyTo, + ) + if err != nil { + err := gtserror.Newf("error seeing if status %s is replyable: %w", status.ID, err) + return gtserror.NewErrorInternalError(err) + } + + if policyResult.Forbidden() { + const errText = "you do not have permission to reply to this status" + err := gtserror.New(errText) + return gtserror.NewErrorForbidden(err, errText) + } + + // Derive pendingApproval status. + var pendingApproval bool + switch { + case policyResult.WithApproval(): + // We're allowed to do + // this pending approval. + pendingApproval = true + + case policyResult.MatchedOnCollection(): + // We're permitted to do this, but since + // we matched due to presence in a followers + // or following collection, we should mark + // as pending approval and wait for an accept. + pendingApproval = true + + case policyResult.Permitted(): + // We're permitted to do this + // based on another kind of match. + pendingApproval = false + } + + status.PendingApproval = &pendingApproval + // Set status fields from inReplyTo. status.InReplyToID = inReplyTo.ID status.InReplyTo = inReplyTo diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go index 49dacf18d..0f5a72b7d 100644 --- a/internal/processing/status/fave.go +++ b/internal/processing/status/fave.go @@ -72,28 +72,73 @@ func (p *Processor) getFaveableStatus( } // FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists). -func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - targetStatus, existingFave, errWithCode := p.getFaveableStatus(ctx, requestingAccount, targetStatusID) +func (p *Processor) FaveCreate( + ctx context.Context, + requester *gtsmodel.Account, + targetStatusID string, +) (*apimodel.Status, gtserror.WithCode) { + status, existingFave, errWithCode := p.getFaveableStatus(ctx, requester, targetStatusID) if errWithCode != nil { return nil, errWithCode } if existingFave != nil { // Status is already faveed. - return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus) + return p.c.GetAPIStatus(ctx, requester, status) } - // Create and store a new fave + // Ensure valid fave target for requester. + policyResult, err := p.intFilter.StatusLikeable(ctx, + requester, + status, + ) + if err != nil { + err := gtserror.Newf("error seeing if status %s is likeable: %w", status.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + if policyResult.Forbidden() { + const errText = "you do not have permission to fave this status" + err := gtserror.New(errText) + return nil, gtserror.NewErrorForbidden(err, errText) + } + + // Derive pendingApproval status. + var pendingApproval bool + switch { + case policyResult.WithApproval(): + // We're allowed to do + // this pending approval. + pendingApproval = true + + case policyResult.MatchedOnCollection(): + // We're permitted to do this, but since + // we matched due to presence in a followers + // or following collection, we should mark + // as pending approval and wait for an accept. + pendingApproval = true + + case policyResult.Permitted(): + // We're permitted to do this + // based on another kind of match. + pendingApproval = false + } + + status.PendingApproval = &pendingApproval + + // Create a new fave, marking it + // as pending approval if necessary. faveID := id.NewULID() gtsFave := >smodel.StatusFave{ ID: faveID, - AccountID: requestingAccount.ID, - Account: requestingAccount, - TargetAccountID: targetStatus.AccountID, - TargetAccount: targetStatus.Account, - StatusID: targetStatus.ID, - Status: targetStatus, - URI: uris.GenerateURIForLike(requestingAccount.Username, faveID), + AccountID: requester.ID, + Account: requester, + TargetAccountID: status.AccountID, + TargetAccount: status.Account, + StatusID: status.ID, + Status: status, + URI: uris.GenerateURIForLike(requester.Username, faveID), + PendingApproval: &pendingApproval, } if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil { @@ -106,11 +151,11 @@ func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel. APObjectType: ap.ActivityLike, APActivityType: ap.ActivityCreate, GTSModel: gtsFave, - Origin: requestingAccount, - Target: targetStatus.Account, + Origin: requester, + Target: status.Account, }) - return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus) + return p.c.GetAPIStatus(ctx, requester, status) } // FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist). diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go index 18f8e741a..7e614cc31 100644 --- a/internal/processing/status/status.go +++ b/internal/processing/status/status.go @@ -19,6 +19,7 @@ package status import ( "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/processing/common" @@ -35,7 +36,8 @@ type Processor struct { state *state.State federator *federation.Federator converter *typeutils.Converter - filter *visibility.Filter + visFilter *visibility.Filter + intFilter *interaction.Filter formatter *text.Formatter parseMention gtsmodel.ParseMentionFunc @@ -50,7 +52,8 @@ func New( polls *polls.Processor, federator *federation.Federator, converter *typeutils.Converter, - filter *visibility.Filter, + visFilter *visibility.Filter, + intFilter *interaction.Filter, parseMention gtsmodel.ParseMentionFunc, ) Processor { return Processor{ @@ -58,7 +61,8 @@ func New( state: state, federator: federator, converter: converter, - filter: filter, + visFilter: visFilter, + intFilter: intFilter, formatter: text.NewFormatter(state.DB), parseMention: parseMention, polls: polls, diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go index 9eba78ec6..f0b22b2c1 100644 --- a/internal/processing/status/status_test.go +++ b/internal/processing/status/status_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" @@ -89,16 +90,30 @@ func (suite *StatusStandardTestSuite) SetupTest() { suite.mediaManager = testrig.NewTestMediaManager(&suite.state) suite.federator = testrig.NewTestFederator(&suite.state, suite.tc, suite.mediaManager) - filter := visibility.NewFilter(&suite.state) + visFilter := visibility.NewFilter(&suite.state) + intFilter := interaction.NewFilter(&suite.state) testrig.StartTimelines( &suite.state, - filter, + visFilter, suite.typeConverter, ) - common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, filter) + common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter) polls := polls.New(&common, &suite.state, suite.typeConverter) - suite.status = status.New(&suite.state, &common, &polls, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(&suite.state, suite.federator)) + + suite.status = status.New( + &suite.state, + &common, + &polls, + suite.federator, + suite.typeConverter, + visFilter, + intFilter, + processing.GetParseMentionFunc( + &suite.state, + suite.federator, + ), + ) testrig.StandardDBSetup(suite.db, suite.testAccounts) testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") diff --git a/internal/processing/stream/statusupdate_test.go b/internal/processing/stream/statusupdate_test.go index 2ae3f217b..f6024686b 100644 --- a/internal/processing/stream/statusupdate_test.go +++ b/internal/processing/stream/statusupdate_test.go @@ -133,19 +133,22 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } diff --git a/internal/processing/timeline/faved.go b/internal/processing/timeline/faved.go index cd3729465..bb7f03fff 100644 --- a/internal/processing/timeline/faved.go +++ b/internal/processing/timeline/faved.go @@ -45,7 +45,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma items := make([]interface{}, 0, count) for _, s := range statuses { - visible, err := p.filter.StatusVisible(ctx, authed.Account, s) + visible, err := p.visFilter.StatusVisible(ctx, authed.Account, s) if err != nil { log.Errorf(ctx, "error checking status visibility: %v", err) continue diff --git a/internal/processing/timeline/home.go b/internal/processing/timeline/home.go index 8bf8dd428..215000933 100644 --- a/internal/processing/timeline/home.go +++ b/internal/processing/timeline/home.go @@ -62,7 +62,7 @@ func HomeTimelineGrab(state *state.State) timeline.GrabFunction { } // HomeTimelineFilter returns a function that satisfies FilterFunction for home timelines. -func HomeTimelineFilter(state *state.State, filter *visibility.Filter) timeline.FilterFunction { +func HomeTimelineFilter(state *state.State, visFilter *visibility.Filter) timeline.FilterFunction { return func(ctx context.Context, accountID string, item timeline.Timelineable) (shouldIndex bool, err error) { status, ok := item.(*gtsmodel.Status) if !ok { @@ -76,7 +76,7 @@ func HomeTimelineFilter(state *state.State, filter *visibility.Filter) timeline. return false, err } - timelineable, err := filter.StatusHomeTimelineable(ctx, requestingAccount, status) + timelineable, err := visFilter.StatusHomeTimelineable(ctx, requestingAccount, status) if err != nil { err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, accountID, err) return false, err diff --git a/internal/processing/timeline/list.go b/internal/processing/timeline/list.go index 2065256e3..a7f5e9d71 100644 --- a/internal/processing/timeline/list.go +++ b/internal/processing/timeline/list.go @@ -62,7 +62,7 @@ func ListTimelineGrab(state *state.State) timeline.GrabFunction { } // ListTimelineFilter returns a function that satisfies FilterFunction for list timelines. -func ListTimelineFilter(state *state.State, filter *visibility.Filter) timeline.FilterFunction { +func ListTimelineFilter(state *state.State, visFilter *visibility.Filter) timeline.FilterFunction { return func(ctx context.Context, listID string, item timeline.Timelineable) (shouldIndex bool, err error) { status, ok := item.(*gtsmodel.Status) if !ok { @@ -82,7 +82,7 @@ func ListTimelineFilter(state *state.State, filter *visibility.Filter) timeline. return false, err } - timelineable, err := filter.StatusHomeTimelineable(ctx, requestingAccount, status) + timelineable, err := visFilter.StatusHomeTimelineable(ctx, requestingAccount, status) if err != nil { err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, list.AccountID, err) return false, err diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go index 0db4080b9..34e6d865d 100644 --- a/internal/processing/timeline/notification.go +++ b/internal/processing/timeline/notification.go @@ -190,7 +190,7 @@ func (p *Processor) notifVisible( return true, nil } - visible, err := p.filter.AccountVisible(ctx, acct, n.OriginAccount) + visible, err := p.visFilter.AccountVisible(ctx, acct, n.OriginAccount) if err != nil { return false, err } @@ -203,7 +203,7 @@ func (p *Processor) notifVisible( // If status is set, ensure it's // visible to notif target. if n.Status != nil { - visible, err := p.filter.StatusVisible(ctx, acct, n.Status) + visible, err := p.visFilter.StatusVisible(ctx, acct, n.Status) if err != nil { return false, err } diff --git a/internal/processing/timeline/public.go b/internal/processing/timeline/public.go index 28062fb2e..dc00688e3 100644 --- a/internal/processing/timeline/public.go +++ b/internal/processing/timeline/public.go @@ -98,7 +98,7 @@ outer: // we end up filtering it out or not. nextMaxIDValue = s.ID - timelineable, err := p.filter.StatusPublicTimelineable(ctx, requester, s) + timelineable, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s) if err != nil { log.Errorf(ctx, "error checking status visibility: %v", err) continue inner diff --git a/internal/processing/timeline/tag.go b/internal/processing/timeline/tag.go index 4320f6adc..811d0bb33 100644 --- a/internal/processing/timeline/tag.go +++ b/internal/processing/timeline/tag.go @@ -128,7 +128,7 @@ func (p *Processor) packageTagResponse( compiledMutes := usermute.NewCompiledUserMuteList(mutes) for _, s := range statuses { - timelineable, err := p.filter.StatusTagTimelineable(ctx, requestingAcct, s) + timelineable, err := p.visFilter.StatusTagTimelineable(ctx, requestingAcct, s) if err != nil { log.Errorf(ctx, "error checking status visibility: %v", err) continue diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go index b791791ee..5966fe864 100644 --- a/internal/processing/timeline/timeline.go +++ b/internal/processing/timeline/timeline.go @@ -26,13 +26,13 @@ import ( type Processor struct { state *state.State converter *typeutils.Converter - filter *visibility.Filter + visFilter *visibility.Filter } -func New(state *state.State, converter *typeutils.Converter, filter *visibility.Filter) Processor { +func New(state *state.State, converter *typeutils.Converter, visFilter *visibility.Filter) Processor { return Processor{ state: state, converter: converter, - filter: filter, + visFilter: visFilter, } } diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go index 705795af4..f08f059ea 100644 --- a/internal/processing/workers/fromfediapi_test.go +++ b/internal/processing/workers/fromfediapi_test.go @@ -45,8 +45,12 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() { testStructs := suite.SetupTestStructs() defer suite.TearDownTestStructs(testStructs) - boostedStatus := suite.testStatuses["local_account_1_status_1"] - boostingAccount := suite.testAccounts["remote_account_1"] + boostedStatus := >smodel.Status{} + *boostedStatus = *suite.testStatuses["local_account_1_status_1"] + + boostingAccount := >smodel.Account{} + *boostingAccount = *suite.testAccounts["remote_account_1"] + announceStatus := >smodel.Status{} announceStatus.URI = "https://example.org/some-announce-uri" announceStatus.BoostOfURI = boostedStatus.URI @@ -64,13 +68,25 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() { Receiving: suite.testAccounts["local_account_1"], Requesting: boostingAccount, }) - suite.NoError(err) + if err != nil { + suite.FailNow(err.Error()) + } - // side effects should be triggered + // Wait for side effects to trigger: // 1. status should have an ID, and be in the database - suite.NotEmpty(announceStatus.ID) - _, err = testStructs.State.DB.GetStatusByID(context.Background(), announceStatus.ID) - suite.NoError(err) + if !testrig.WaitFor(func() bool { + if announceStatus.ID == "" { + return false + } + + _, err = testStructs.State.DB.GetStatusByID( + context.Background(), + announceStatus.ID, + ) + return err == nil + }) { + suite.FailNow("timed out waiting for announce to be in the database") + } // 2. a notification should exist for the announce where := []db.Where{ @@ -89,78 +105,89 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() { suite.False(*notif.Read) } -// Todo: fix this test up in interaction policies PR. -// func (suite *FromFediAPITestSuite) TestProcessReplyMention() { -// testStructs := suite.SetupTestStructs() -// defer suite.TearDownTestStructs(testStructs) +func (suite *FromFediAPITestSuite) TestProcessReplyMention() { + testStructs := suite.SetupTestStructs() + defer suite.TearDownTestStructs(testStructs) -// repliedAccount := suite.testAccounts["local_account_1"] -// repliedStatus := suite.testStatuses["local_account_1_status_1"] -// replyingAccount := suite.testAccounts["remote_account_1"] + repliedAccount := >smodel.Account{} + *repliedAccount = *suite.testAccounts["local_account_1"] -// // Set the replyingAccount's last fetched_at -// // date to something recent so no refresh is attempted, -// // and ensure it isn't a suspended account. -// replyingAccount.FetchedAt = time.Now() -// replyingAccount.SuspendedAt = time.Time{} -// replyingAccount.SuspensionOrigin = "" -// err := testStructs.State.DB.UpdateAccount(context.Background(), -// replyingAccount, -// "fetched_at", -// "suspended_at", -// "suspension_origin", -// ) -// suite.NoError(err) + repliedStatus := >smodel.Status{} + *repliedStatus = *suite.testStatuses["local_account_1_status_1"] -// // Get replying statusable to use from remote test statuses. -// const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552" -// replyingStatusable := testrig.NewTestFediStatuses()[replyingURI] -// ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI)) + replyingAccount := >smodel.Account{} + *replyingAccount = *suite.testAccounts["remote_account_1"] -// // Open a websocket stream to later test the streamed status reply. -// wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) -// suite.NoError(errWithCode) + // Set the replyingAccount's last fetched_at + // date to something recent so no refresh is attempted, + // and ensure it isn't a suspended account. + replyingAccount.FetchedAt = time.Now() + replyingAccount.SuspendedAt = time.Time{} + replyingAccount.SuspensionOrigin = "" + err := testStructs.State.DB.UpdateAccount(context.Background(), + replyingAccount, + "fetched_at", + "suspended_at", + "suspension_origin", + ) + suite.NoError(err) -// // Send the replied status off to the fedi worker to be further processed. -// err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{ -// APObjectType: ap.ObjectNote, -// APActivityType: ap.ActivityCreate, -// APObject: replyingStatusable, -// Receiving: repliedAccount, -// Requesting: replyingAccount, -// }) -// suite.NoError(err) + // Get replying statusable to use from remote test statuses. + const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552" + replyingStatusable := testrig.NewTestFediStatuses()[replyingURI] + ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI)) -// // side effects should be triggered -// // 1. status should be in the database -// replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI) -// suite.NoError(err) + // Open a websocket stream to later test the streamed status reply. + wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) + suite.NoError(errWithCode) -// // 2. a notification should exist for the mention -// var notif gtsmodel.Notification -// err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{ -// {Key: "status_id", Value: replyingStatus.ID}, -// }, ¬if) -// suite.NoError(err) -// suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) -// suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) -// suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) -// suite.Equal(replyingStatus.ID, notif.StatusID) -// suite.False(*notif.Read) + // Send the replied status off to the fedi worker to be further processed. + err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityCreate, + APObject: replyingStatusable, + Receiving: repliedAccount, + Requesting: replyingAccount, + }) + if err != nil { + suite.FailNow(err.Error()) + } -// ctx, _ := context.WithTimeout(context.Background(), time.Second*5) -// msg, ok := wssStream.Recv(ctx) -// suite.True(ok) + // Wait for side effects to trigger: + // 1. status should be in the database + var replyingStatus *gtsmodel.Status + if !testrig.WaitFor(func() bool { + replyingStatus, err = testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI) + return err == nil + }) { + suite.FailNow("timed out waiting for replying status to be in the database") + } -// suite.Equal(stream.EventTypeNotification, msg.Event) -// suite.NotEmpty(msg.Payload) -// suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) -// notifStreamed := &apimodel.Notification{} -// err = json.Unmarshal([]byte(msg.Payload), notifStreamed) -// suite.NoError(err) -// suite.Equal("mention", notifStreamed.Type) -// suite.Equal(replyingAccount.ID, notifStreamed.Account.ID) -// } + // 2. a notification should exist for the mention + var notif gtsmodel.Notification + err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{ + {Key: "status_id", Value: replyingStatus.ID}, + }, ¬if) + suite.NoError(err) + suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) + suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) + suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) + suite.Equal(replyingStatus.ID, notif.StatusID) + suite.False(*notif.Read) + + ctx, _ := context.WithTimeout(context.Background(), time.Second*5) + msg, ok := wssStream.Recv(ctx) + suite.True(ok) + + suite.Equal(stream.EventTypeNotification, msg.Event) + suite.NotEmpty(msg.Payload) + suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) + notifStreamed := &apimodel.Notification{} + err = json.Unmarshal([]byte(msg.Payload), notifStreamed) + suite.NoError(err) + suite.Equal("mention", notifStreamed.Type) + suite.Equal(replyingAccount.ID, notifStreamed.Account.ID) +} func (suite *FromFediAPITestSuite) TestProcessFave() { testStructs := suite.SetupTestStructs() @@ -305,8 +332,11 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() { ctx := context.Background() - deletedAccount := suite.testAccounts["remote_account_1"] - receivingAccount := suite.testAccounts["local_account_1"] + deletedAccount := >smodel.Account{} + *deletedAccount = *suite.testAccounts["remote_account_1"] + + receivingAccount := >smodel.Account{} + *receivingAccount = *suite.testAccounts["local_account_1"] // before doing the delete.... // make local_account_1 and remote_account_1 into mufos diff --git a/internal/processing/workers/surface.go b/internal/processing/workers/surface.go index 1a7dbbfe5..4f6597b9a 100644 --- a/internal/processing/workers/surface.go +++ b/internal/processing/workers/surface.go @@ -36,7 +36,7 @@ type Surface struct { State *state.State Converter *typeutils.Converter Stream *stream.Processor - Filter *visibility.Filter + VisFilter *visibility.Filter EmailSender email.Sender Conversations *conversations.Processor } diff --git a/internal/processing/workers/surfacenotify_test.go b/internal/processing/workers/surfacenotify_test.go index 937ddeca2..876f69933 100644 --- a/internal/processing/workers/surfacenotify_test.go +++ b/internal/processing/workers/surfacenotify_test.go @@ -42,7 +42,7 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() { State: testStructs.State, Converter: testStructs.TypeConverter, Stream: testStructs.Processor.Stream(), - Filter: visibility.NewFilter(testStructs.State), + VisFilter: visibility.NewFilter(testStructs.State), EmailSender: testStructs.EmailSender, Conversations: testStructs.Processor.Conversations(), } diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go index 8ac8293ed..7bd0a51c6 100644 --- a/internal/processing/workers/surfacetimeline.go +++ b/internal/processing/workers/surfacetimeline.go @@ -109,7 +109,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( // If it's not timelineable, we can just stop early, since lists // are prettymuch subsets of the home timeline, so if it shouldn't // appear there, it shouldn't appear in lists either. - timelineable, err := s.Filter.StatusHomeTimelineable( + timelineable, err := s.VisFilter.StatusHomeTimelineable( ctx, follow.Account, status, ) if err != nil { @@ -482,7 +482,7 @@ func (s *Surface) timelineStatusUpdateForFollowers( // If it's not timelineable, we can just stop early, since lists // are prettymuch subsets of the home timeline, so if it shouldn't // appear there, it shouldn't appear in lists either. - timelineable, err := s.Filter.StatusHomeTimelineable( + timelineable, err := s.VisFilter.StatusHomeTimelineable( ctx, follow.Account, status, ) if err != nil { diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go index c7f67b025..04010a92e 100644 --- a/internal/processing/workers/workers.go +++ b/internal/processing/workers/workers.go @@ -40,7 +40,7 @@ func New( state *state.State, federator *federation.Federator, converter *typeutils.Converter, - filter *visibility.Filter, + visFilter *visibility.Filter, emailSender email.Sender, account *account.Processor, media *media.Processor, @@ -61,7 +61,7 @@ func New( State: state, Converter: converter, Stream: stream, - Filter: filter, + VisFilter: visFilter, EmailSender: emailSender, Conversations: conversations, } diff --git a/internal/processing/workers/workers_test.go b/internal/processing/workers/workers_test.go index 3093fd93a..65ed3f6b7 100644 --- a/internal/processing/workers/workers_test.go +++ b/internal/processing/workers/workers_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/cleaner" "github.com/superseriousbusiness/gotosocial/internal/email" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -160,7 +161,18 @@ func (suite *WorkersTestSuite) SetupTestStructs() *TestStructs { oauthServer := testrig.NewTestOauthServer(db) emailSender := testrig.NewEmailSender("../../../web/template/", nil) - processor := processing.NewProcessor(cleaner.New(&state), typeconverter, federator, oauthServer, mediaManager, &state, emailSender) + processor := processing.NewProcessor( + cleaner.New(&state), + typeconverter, + federator, + oauthServer, + mediaManager, + &state, + emailSender, + visibility.NewFilter(&state), + interaction.NewFilter(&state), + ) + testrig.StartWorkers(&state, processor.Workers()) testrig.StandardDBSetup(db, suite.testAccounts) diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index dfa72fdcd..311839dc0 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -20,6 +20,7 @@ package typeutils import ( "sync" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/state" ) @@ -28,13 +29,15 @@ type Converter struct { state *state.State defaultAvatars []string randAvatars sync.Map - filter *visibility.Filter + visFilter *visibility.Filter + intFilter *interaction.Filter } func NewConverter(state *state.State) *Converter { return &Converter{ state: state, defaultAvatars: populateDefaultAvatars(), - filter: visibility.NewFilter(state), + visFilter: visibility.NewFilter(state), + intFilter: interaction.NewFilter(state), } } diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 29d972e48..cbe746d2f 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -861,7 +861,7 @@ func (c *Converter) statusToAPIFilterResults( for _, account := range otherAccounts { // Is this account visible? - visible, err := c.filter.AccountVisible(ctx, requestingAccount, account) + visible, err := c.visFilter.AccountVisible(ctx, requestingAccount, account) if err != nil { return nil, err } @@ -2382,8 +2382,8 @@ func (c *Converter) ThemesToAPIThemes(themes []*gtsmodel.Theme) []apimodel.Theme func (c *Converter) InteractionPolicyToAPIInteractionPolicy( ctx context.Context, policy *gtsmodel.InteractionPolicy, - _ *gtsmodel.Status, // Used in upcoming PR. - _ *gtsmodel.Account, // Used in upcoming PR. + status *gtsmodel.Status, + requester *gtsmodel.Account, ) (*apimodel.InteractionPolicy, error) { apiPolicy := &apimodel.InteractionPolicy{ CanFavourite: apimodel.PolicyRules{ @@ -2400,6 +2400,75 @@ func (c *Converter) InteractionPolicyToAPIInteractionPolicy( }, } + if status == nil || requester == nil { + // We're done here! + return apiPolicy, nil + } + + // Status and requester are both defined, + // so we can add the "me" Value to the policy + // for each interaction type, if applicable. + + likeable, err := c.intFilter.StatusLikeable(ctx, requester, status) + if err != nil { + err := gtserror.Newf("error checking status likeable by requester: %w", err) + return nil, err + } + + if likeable.Permission == gtsmodel.PolicyPermissionPermitted { + // We can do this! + apiPolicy.CanFavourite.Always = append( + apiPolicy.CanFavourite.Always, + apimodel.PolicyValueMe, + ) + } else if likeable.Permission == gtsmodel.PolicyPermissionWithApproval { + // We can do this with approval. + apiPolicy.CanFavourite.WithApproval = append( + apiPolicy.CanFavourite.WithApproval, + apimodel.PolicyValueMe, + ) + } + + replyable, err := c.intFilter.StatusReplyable(ctx, requester, status) + if err != nil { + err := gtserror.Newf("error checking status replyable by requester: %w", err) + return nil, err + } + + if replyable.Permission == gtsmodel.PolicyPermissionPermitted { + // We can do this! + apiPolicy.CanReply.Always = append( + apiPolicy.CanReply.Always, + apimodel.PolicyValueMe, + ) + } else if replyable.Permission == gtsmodel.PolicyPermissionWithApproval { + // We can do this with approval. + apiPolicy.CanReply.WithApproval = append( + apiPolicy.CanReply.WithApproval, + apimodel.PolicyValueMe, + ) + } + + boostable, err := c.intFilter.StatusBoostable(ctx, requester, status) + if err != nil { + err := gtserror.Newf("error checking status boostable by requester: %w", err) + return nil, err + } + + if boostable.Permission == gtsmodel.PolicyPermissionPermitted { + // We can do this! + apiPolicy.CanReblog.Always = append( + apiPolicy.CanReblog.Always, + apimodel.PolicyValueMe, + ) + } else if boostable.Permission == gtsmodel.PolicyPermissionWithApproval { + // We can do this with approval. + apiPolicy.CanReblog.WithApproval = append( + apiPolicy.CanReblog.WithApproval, + apimodel.PolicyValueMe, + ) + } + return apiPolicy, nil } diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 579b7a067..46f6c2455 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -551,19 +551,22 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -747,19 +750,22 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -927,19 +933,22 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -1010,19 +1019,22 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -1257,19 +1269,22 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -1560,19 +1575,22 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage() "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } @@ -2561,19 +2579,22 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "interaction_policy": { "can_favourite": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reply": { "always": [ - "public" + "public", + "me" ], "with_approval": [] }, "can_reblog": { "always": [ - "public" + "public", + "me" ], "with_approval": [] } diff --git a/testrig/federator.go b/testrig/federator.go index f90aa99ab..cd4f38b10 100644 --- a/testrig/federator.go +++ b/testrig/federator.go @@ -19,6 +19,7 @@ package testrig import ( "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -28,5 +29,13 @@ import ( // NewTestFederator returns a federator with the given database and (mock!!) transport controller. func NewTestFederator(state *state.State, tc transport.Controller, mediaManager *media.Manager) *federation.Federator { - return federation.NewFederator(state, NewTestFederatingDB(state), tc, typeutils.NewConverter(state), visibility.NewFilter(state), mediaManager) + return federation.NewFederator( + state, + NewTestFederatingDB(state), + tc, + typeutils.NewConverter(state), + visibility.NewFilter(state), + interaction.NewFilter(state), + mediaManager, + ) } diff --git a/testrig/processor.go b/testrig/processor.go index a1bab4f9a..e098de33a 100644 --- a/testrig/processor.go +++ b/testrig/processor.go @@ -21,6 +21,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/cleaner" "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/filter/interaction" + "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -31,5 +33,15 @@ import ( // The passed in state will have its worker functions set appropriately, // but the state will not be initialized. func NewTestProcessor(state *state.State, federator *federation.Federator, emailSender email.Sender, mediaManager *media.Manager) *processing.Processor { - return processing.NewProcessor(cleaner.New(state), typeutils.NewConverter(state), federator, NewTestOauthServer(state.DB), mediaManager, state, emailSender) + return processing.NewProcessor( + cleaner.New(state), + typeutils.NewConverter(state), + federator, + NewTestOauthServer(state.DB), + mediaManager, + state, + emailSender, + visibility.NewFilter(state), + interaction.NewFilter(state), + ) } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 87f8c7054..218668a69 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -1436,6 +1436,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "admin_account_status_2": { ID: "01F8MHAAY43M6RJ473VQFCVH37", @@ -1459,6 +1460,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "admin_account_status_3": { ID: "01FF25D5Q0DH7CHD57CTRS6WK0", @@ -1483,6 +1485,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "admin_account_status_4": { ID: "01G36SF3V6Y6V5BF9P4R7PQG7G", @@ -1504,6 +1507,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_1_status_1": { ID: "01F8MHAMCHF6Y650WCRSCP4WMY", @@ -1526,6 +1530,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_1_status_2": { ID: "01F8MHAYFKS4KMXF8K5Y1C0KRN", @@ -1548,6 +1553,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(false), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_1_status_3": { ID: "01F8MHBBN8120SYH7D5S050MGK", @@ -1581,6 +1587,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { }, }, ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_1_status_4": { ID: "01F8MH82FYRXD2RC6108DAJ5HB", @@ -1604,6 +1611,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_1_status_5": { ID: "01FCTA44PW9H1TB328S9AQXKDS", @@ -1627,6 +1635,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_1_status_6": { ID: "01HEN2RZ8BG29Y5Z9VJC73HZW7", @@ -1651,6 +1660,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Federated: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEN2RKT1YTEZ80SA8HGP105F", + PendingApproval: util.Ptr(false), }, "local_account_1_status_7": { ID: "01HH9KYNQPA416TNJ53NSATP40", @@ -1673,6 +1683,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_1_status_8": { ID: "01J2M1HPFSS54S60Y0KYV23KJE", @@ -1720,6 +1731,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_2_status_2": { ID: "01F8MHC0H0A7XHTVH5F596ZKBM", @@ -1753,6 +1765,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { }, }, ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_2_status_3": { ID: "01F8MHC8VWDRBQR0N1BATDDEM5", @@ -1787,6 +1800,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { }, }, ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_2_status_4": { ID: "01F8MHCP5P2NWYQ416SBA0XSEV", @@ -1820,6 +1834,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { }, }, ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_2_status_5": { ID: "01FCQSQ667XHJ9AV9T27SJJSX5", @@ -1845,6 +1860,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_2_status_6": { ID: "01FN3VJGFH10KR7S2PB0GFJZYG", @@ -1870,6 +1886,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_2_status_7": { ID: "01G20ZM733MGN8J344T4ZDDFY1", @@ -1894,6 +1911,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "local_account_2_status_8": { ID: "01HEN2PRXT0TF4YDRA64FZZRN7", @@ -1918,6 +1936,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Federated: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEN2QB5NR4NCEHGYC3HN84K6", + PendingApproval: util.Ptr(false), }, "remote_account_1_status_1": { ID: "01FVW7JHQFSFK166WWKR8CBA6M", @@ -1942,6 +1961,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, "remote_account_1_status_2": { ID: "01HEN2QRFA8H3C6QPN7RD4KSR6", @@ -1966,6 +1986,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Federated: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEN2R65468ZG657C4ZPHJ4EX", + PendingApproval: util.Ptr(false), }, "remote_account_1_status_3": { ID: "01HEWV37MHV8BAC8ANFGVRRM5D", @@ -1990,6 +2011,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Federated: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEWV1GW2D49R919NPEDXPTZ5", + PendingApproval: util.Ptr(false), }, "remote_account_2_status_1": { ID: "01HE7XJ1CG84TBKH5V9XKBVGF5", @@ -2014,6 +2036,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "", Federated: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, + PendingApproval: util.Ptr(false), }, } } diff --git a/testrig/util.go b/testrig/util.go index abc94bf02..31312f0af 100644 --- a/testrig/util.go +++ b/testrig/util.go @@ -91,10 +91,10 @@ func StopWorkers(state *state.State) { state.Workers.Dereference.Stop() } -func StartTimelines(state *state.State, filter *visibility.Filter, converter *typeutils.Converter) { +func StartTimelines(state *state.State, visFilter *visibility.Filter, converter *typeutils.Converter) { state.Timelines.Home = timeline.NewManager( tlprocessor.HomeTimelineGrab(state), - tlprocessor.HomeTimelineFilter(state, filter), + tlprocessor.HomeTimelineFilter(state, visFilter), tlprocessor.HomeTimelineStatusPrepare(state, converter), tlprocessor.SkipInsert(), ) @@ -104,7 +104,7 @@ func StartTimelines(state *state.State, filter *visibility.Filter, converter *ty state.Timelines.List = timeline.NewManager( tlprocessor.ListTimelineGrab(state), - tlprocessor.ListTimelineFilter(state, filter), + tlprocessor.ListTimelineFilter(state, visFilter), tlprocessor.ListTimelineStatusPrepare(state, converter), tlprocessor.SkipInsert(), )