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(),
)