mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-24 12:46:38 +00:00
[chore] Add interaction filter to complement existing visibility filter (#3111)
* [chore] Add interaction filter to complement existing visibility filter
* pass in ptr to visibility and interaction filters to Processor{} to ensure shared
* use int constants for for match type, cache db calls in filterctx
* function name typo 😇
---------
Co-authored-by: kim <grufwub@gmail.com>
This commit is contained in:
parent
63fc9b6c3e
commit
c9b6220fef
|
@ -36,6 +36,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
"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/spam"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
@ -190,10 +191,19 @@
|
||||||
oauthServer := oauth.New(ctx, dbService)
|
oauthServer := oauth.New(ctx, dbService)
|
||||||
typeConverter := typeutils.NewConverter(state)
|
typeConverter := typeutils.NewConverter(state)
|
||||||
visFilter := visibility.NewFilter(state)
|
visFilter := visibility.NewFilter(state)
|
||||||
|
intFilter := interaction.NewFilter(state)
|
||||||
spamFilter := spam.NewFilter(state)
|
spamFilter := spam.NewFilter(state)
|
||||||
federatingDB := federatingdb.New(state, typeConverter, visFilter, spamFilter)
|
federatingDB := federatingdb.New(state, typeConverter, visFilter, spamFilter)
|
||||||
transportController := transport.NewController(state, federatingDB, &federation.Clock{}, client)
|
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
|
// Decide whether to create a noop email
|
||||||
// sender (won't send emails) or a real one.
|
// sender (won't send emails) or a real one.
|
||||||
|
@ -268,6 +278,8 @@ func(context.Context, time.Time) {
|
||||||
mediaManager,
|
mediaManager,
|
||||||
state,
|
state,
|
||||||
emailSender,
|
emailSender,
|
||||||
|
visFilter,
|
||||||
|
intFilter,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize the specialized workers pools.
|
// Initialize the specialized workers pools.
|
||||||
|
|
|
@ -532,19 +532,22 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -774,19 +777,22 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -1016,19 +1022,22 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,43 +173,42 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to boost a status that's not boostable / visible to us
|
// try to boost a status that's not boostable / visible to us
|
||||||
// TODO: sort this out with new interaction policies
|
func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
||||||
// func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
t := suite.testTokens["local_account_1"]
|
||||||
// t := suite.testTokens["local_account_1"]
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
// oauthToken := oauth.DBTokenToToken(t)
|
|
||||||
|
|
||||||
// targetStatus := suite.testStatuses["local_account_2_status_4"]
|
targetStatus := suite.testStatuses["local_account_2_status_4"]
|
||||||
|
|
||||||
// // setup
|
// setup
|
||||||
// recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
// ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["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 = 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")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
// // normally the router would populate these params from the path values,
|
// 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.
|
// but because we're calling the function directly, we need to set them manually.
|
||||||
// ctx.Params = gin.Params{
|
ctx.Params = gin.Params{
|
||||||
// gin.Param{
|
gin.Param{
|
||||||
// Key: statuses.IDKey,
|
Key: statuses.IDKey,
|
||||||
// Value: targetStatus.ID,
|
Value: targetStatus.ID,
|
||||||
// },
|
},
|
||||||
// }
|
}
|
||||||
|
|
||||||
// suite.statusModule.StatusBoostPOSTHandler(ctx)
|
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||||
|
|
||||||
// // check response
|
// check response
|
||||||
// suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses
|
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||||
|
|
||||||
// result := recorder.Result()
|
result := recorder.Result()
|
||||||
// defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
// suite.NoError(err)
|
suite.NoError(err)
|
||||||
// suite.Equal(`{"error":"Not Found"}`, string(b))
|
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
|
// try to boost a status that's not visible to the user
|
||||||
func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
||||||
|
|
|
@ -89,43 +89,42 @@ func (suite *StatusFaveTestSuite) TestPostFave() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to fave a status that's not faveable
|
// try to fave a status that's not faveable
|
||||||
// TODO: replace this when interaction policies enforced.
|
func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||||
// func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
t := suite.testTokens["admin_account"]
|
||||||
// t := suite.testTokens["local_account_1"]
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
// 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
|
// setup
|
||||||
// recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
// ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["admin_account"])
|
||||||
// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
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 = 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")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
// // normally the router would populate these params from the path values,
|
// 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.
|
// but because we're calling the function directly, we need to set them manually.
|
||||||
// ctx.Params = gin.Params{
|
ctx.Params = gin.Params{
|
||||||
// gin.Param{
|
gin.Param{
|
||||||
// Key: statuses.IDKey,
|
Key: statuses.IDKey,
|
||||||
// Value: targetStatus.ID,
|
Value: targetStatus.ID,
|
||||||
// },
|
},
|
||||||
// }
|
}
|
||||||
|
|
||||||
// suite.statusModule.StatusFavePOSTHandler(ctx)
|
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||||
|
|
||||||
// // check response
|
// check response
|
||||||
// suite.EqualValues(http.StatusForbidden, recorder.Code)
|
suite.EqualValues(http.StatusForbidden, recorder.Code)
|
||||||
|
|
||||||
// result := recorder.Result()
|
result := recorder.Result()
|
||||||
// defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
// assert.NoError(suite.T(), err)
|
assert.NoError(suite.T(), err)
|
||||||
// assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b))
|
assert.Equal(suite.T(), `{"error":"Forbidden: you do not have permission to fave this status"}`, string(b))
|
||||||
// }
|
}
|
||||||
|
|
||||||
func TestStatusFaveTestSuite(t *testing.T) {
|
func TestStatusFaveTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(StatusFaveTestSuite))
|
suite.Run(t, new(StatusFaveTestSuite))
|
||||||
|
|
|
@ -151,19 +151,22 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -236,19 +239,22 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
|
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"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/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
@ -85,7 +87,19 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom
|
||||||
config.SetAccountDomain(accountDomain)
|
config.SetAccountDomain(accountDomain)
|
||||||
testrig.StopWorkers(&suite.state)
|
testrig.StopWorkers(&suite.state)
|
||||||
testrig.StartNoopWorkers(&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)
|
suite.webfingerModule = webfinger.New(suite.processor)
|
||||||
testrig.StartNoopWorkers(&suite.state)
|
testrig.StartNoopWorkers(&suite.state)
|
||||||
|
|
||||||
|
|
|
@ -69,12 +69,6 @@ func (d *Dereferencer) EnrichAnnounce(
|
||||||
return nil, err
|
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
|
// Set boost_of_uri again in case the
|
||||||
// original URI was an indirect link.
|
// original URI was an indirect link.
|
||||||
boost.BoostOfURI = target.URI
|
boost.BoostOfURI = target.URI
|
||||||
|
@ -92,6 +86,24 @@ func (d *Dereferencer) EnrichAnnounce(
|
||||||
boost.Visibility = target.Visibility
|
boost.Visibility = target.Visibility
|
||||||
boost.Federated = target.Federated
|
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.
|
// Store the boost wrapper status in database.
|
||||||
switch err = d.state.DB.PutStatus(ctx, boost); {
|
switch err = d.state.DB.PutStatus(ctx, boost); {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -83,7 +84,8 @@ type Dereferencer struct {
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
transportController transport.Controller
|
transportController transport.Controller
|
||||||
mediaManager *media.Manager
|
mediaManager *media.Manager
|
||||||
visibility *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
|
intFilter *interaction.Filter
|
||||||
|
|
||||||
// in-progress dereferencing media / emoji
|
// in-progress dereferencing media / emoji
|
||||||
derefMedia map[string]*media.ProcessingMedia
|
derefMedia map[string]*media.ProcessingMedia
|
||||||
|
@ -102,12 +104,14 @@ type Dereferencer struct {
|
||||||
handshakesMu sync.Mutex
|
handshakesMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDereferencer returns a Dereferencer initialized with the given parameters.
|
// NewDereferencer returns a Dereferencer
|
||||||
|
// initialized with the given parameters.
|
||||||
func NewDereferencer(
|
func NewDereferencer(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
transportController transport.Controller,
|
transportController transport.Controller,
|
||||||
visFilter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
|
intFilter *interaction.Filter,
|
||||||
mediaManager *media.Manager,
|
mediaManager *media.Manager,
|
||||||
) Dereferencer {
|
) Dereferencer {
|
||||||
return Dereferencer{
|
return Dereferencer{
|
||||||
|
@ -115,7 +119,8 @@ func NewDereferencer(
|
||||||
converter: converter,
|
converter: converter,
|
||||||
transportController: transportController,
|
transportController: transportController,
|
||||||
mediaManager: mediaManager,
|
mediaManager: mediaManager,
|
||||||
visibility: visFilter,
|
visFilter: visFilter,
|
||||||
|
intFilter: intFilter,
|
||||||
derefMedia: make(map[string]*media.ProcessingMedia),
|
derefMedia: make(map[string]*media.ProcessingMedia),
|
||||||
derefEmojis: make(map[string]*media.ProcessingEmoji),
|
derefEmojis: make(map[string]*media.ProcessingEmoji),
|
||||||
handshakes: make(map[string][]*url.URL),
|
handshakes: make(map[string][]*url.URL),
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -79,8 +80,19 @@ func (suite *DereferencerStandardTestSuite) SetupTest() {
|
||||||
suite.state.Storage = suite.storage
|
suite.state.Storage = suite.storage
|
||||||
|
|
||||||
visFilter := visibility.NewFilter(&suite.state)
|
visFilter := visibility.NewFilter(&suite.state)
|
||||||
|
intFilter := interaction.NewFilter(&suite.state)
|
||||||
media := testrig.NewTestMediaManager(&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)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -502,7 +502,8 @@ func (d *Dereferencer) enrichStatus(
|
||||||
latestStatus.Local = status.Local
|
latestStatus.Local = status.Local
|
||||||
|
|
||||||
// Check if this is a permitted status we should accept.
|
// 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 {
|
if err != nil {
|
||||||
return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err)
|
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
|
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(
|
func (d *Dereferencer) fetchStatusMentions(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requestUser string,
|
requestUser string,
|
||||||
|
|
216
internal/federation/dereferencing/status_permitted.go
Normal file
216
internal/federation/dereferencing/status_permitted.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
@ -68,6 +69,7 @@ func (suite *FederatingActorTestSuite) TestSendNoRemoteFollowers() {
|
||||||
tc,
|
tc,
|
||||||
suite.typeconverter,
|
suite.typeconverter,
|
||||||
visibility.NewFilter(&suite.state),
|
visibility.NewFilter(&suite.state),
|
||||||
|
interaction.NewFilter(&suite.state),
|
||||||
testrig.NewTestMediaManager(&suite.state),
|
testrig.NewTestMediaManager(&suite.state),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -122,6 +124,7 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() {
|
||||||
tc,
|
tc,
|
||||||
suite.typeconverter,
|
suite.typeconverter,
|
||||||
visibility.NewFilter(&suite.state),
|
visibility.NewFilter(&suite.state),
|
||||||
|
interaction.NewFilter(&suite.state),
|
||||||
testrig.NewTestMediaManager(&suite.state),
|
testrig.NewTestMediaManager(&suite.state),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -52,6 +53,7 @@ func NewFederator(
|
||||||
transportController transport.Controller,
|
transportController transport.Controller,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
visFilter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
|
intFilter *interaction.Filter,
|
||||||
mediaManager *media.Manager,
|
mediaManager *media.Manager,
|
||||||
) *Federator {
|
) *Federator {
|
||||||
clock := &Clock{}
|
clock := &Clock{}
|
||||||
|
@ -62,7 +64,14 @@ func NewFederator(
|
||||||
converter: converter,
|
converter: converter,
|
||||||
transportController: transportController,
|
transportController: transportController,
|
||||||
mediaManager: mediaManager,
|
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)
|
actor := newFederatingActor(f, f, federatingDB, clock)
|
||||||
f.actor = actor
|
f.actor = actor
|
||||||
|
|
34
internal/filter/interaction/filter.go
Normal file
34
internal/filter/interaction/filter.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
561
internal/filter/interaction/interactable.go
Normal file
561
internal/filter/interaction/interactable.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package visibility
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StatusBoostable checks if given status is boostable by requester, checking boolean status visibility to requester and ultimately the AP status visibility setting.
|
|
||||||
func (f *Filter) StatusBoostable(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
|
||||||
if status.Visibility == gtsmodel.VisibilityDirect {
|
|
||||||
log.Trace(ctx, "direct statuses are not boostable")
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether status is visible to requesting account.
|
|
||||||
visible, err := f.StatusVisible(ctx, requester, status)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
log.Trace(ctx, "status not visible to requesting account")
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if requester.ID == status.AccountID {
|
|
||||||
// Status author can always boost non-directs.
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.Visibility == gtsmodel.VisibilityFollowersOnly ||
|
|
||||||
status.Visibility == gtsmodel.VisibilityMutualsOnly {
|
|
||||||
log.Trace(ctx, "unauthored %s status not boostable", status.Visibility)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
// GoToSocial
|
|
||||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package visibility_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StatusBoostableTestSuite struct {
|
|
||||||
FilterStandardTestSuite
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOwnPublicBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_1"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.True(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOwnUnlockedBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_2"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.True(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOwnMutualsOnlyNonInteractiveBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_3"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.True(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOwnMutualsOnlyBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_4"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.True(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOwnFollowersOnlyBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_5"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.True(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOwnDirectNotBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_2_status_6"]
|
|
||||||
testAccount := suite.testAccounts["local_account_2"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.False(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOtherPublicBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_2_status_1"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.True(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOtherUnlistedBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_2"]
|
|
||||||
testAccount := suite.testAccounts["local_account_2"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.True(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOtherFollowersOnlyNotBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_2_status_7"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.False(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestOtherDirectNotBoostable() {
|
|
||||||
testStatus := suite.testStatuses["local_account_2_status_6"]
|
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.False(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *StatusBoostableTestSuite) TestRemoteFollowersOnlyNotVisible() {
|
|
||||||
testStatus := suite.testStatuses["local_account_1_status_5"]
|
|
||||||
testAccount := suite.testAccounts["remote_account_1"]
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus)
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
suite.False(boostable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatusBoostableTestSuite(t *testing.T) {
|
|
||||||
suite.Run(t, new(StatusBoostableTestSuite))
|
|
||||||
}
|
|
|
@ -25,6 +25,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"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.
|
// 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()
|
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.
|
// StatusVisible will check if status is visible to requester,
|
||||||
func (f *Filter) StatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
// 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
|
const vtype = cache.VisibilityTypeStatus
|
||||||
|
|
||||||
// By default we assume no auth.
|
// By default we assume no auth.
|
||||||
|
@ -75,8 +83,14 @@ func (f *Filter) StatusVisible(ctx context.Context, requester *gtsmodel.Account,
|
||||||
return visibility.Value, nil
|
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.
|
// isStatusVisible will check if status is visible to requester.
|
||||||
func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
// 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.
|
// Ensure that status is fully populated for further processing.
|
||||||
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
|
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
|
||||||
return false, gtserror.Newf("error populating status %s: %w", status.ID, err)
|
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
|
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 {
|
if status.Visibility == gtsmodel.VisibilityPublic {
|
||||||
// This status will be visible to all.
|
// This status will be visible to all.
|
||||||
return true, nil
|
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.
|
// 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) {
|
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.
|
// Check whether status author's account is visible to requester.
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatusVisibleTestSuite struct {
|
type StatusVisibleTestSuite struct {
|
||||||
|
@ -156,6 +157,49 @@ func (suite *StatusVisibleTestSuite) TestStatusNotVisibleIfNotFollowingCached()
|
||||||
suite.False(visible)
|
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) {
|
func TestStatusVisibleTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(StatusVisibleTestSuite))
|
suite.Run(t, new(StatusVisibleTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,26 +111,74 @@ func (p PolicyValue) FeasibleForVisibility(v Visibility) bool {
|
||||||
|
|
||||||
type PolicyValues []PolicyValue
|
type PolicyValues []PolicyValue
|
||||||
|
|
||||||
// PolicyResult represents the result of
|
// PolicyPermission represents the permission
|
||||||
// checking an Actor URI and interaction
|
// state for a certain Actor URI and interaction
|
||||||
// type against the conditions of an
|
// type, in relation to a policy.
|
||||||
// InteractionPolicy to determine if that
|
type PolicyPermission int
|
||||||
// interaction is permitted.
|
|
||||||
type PolicyResult int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Interaction is forbidden for this
|
// Interaction is forbidden for this
|
||||||
// PolicyValue + interaction combination.
|
// PolicyValue + interaction combination.
|
||||||
PolicyResultForbidden PolicyResult = iota
|
PolicyPermissionForbidden PolicyPermission = iota
|
||||||
// Interaction is conditionally permitted
|
// Interaction is conditionally permitted
|
||||||
// for this PolicyValue + interaction combo,
|
// for this PolicyValue + interaction combo,
|
||||||
// pending approval by the item owner.
|
// pending approval by the item owner.
|
||||||
PolicyResultWithApproval
|
PolicyPermissionWithApproval
|
||||||
// Interaction is permitted for this
|
// Interaction is permitted for this
|
||||||
// PolicyValue + interaction combination.
|
// 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
|
// An InteractionPolicy determines which
|
||||||
// interactions will be accepted for an
|
// interactions will be accepted for an
|
||||||
// item, and according to what rules.
|
// item, and according to what rules.
|
||||||
|
|
|
@ -38,7 +38,7 @@ type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
mediaManager *media.Manager
|
mediaManager *media.Manager
|
||||||
filter *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
formatter *text.Formatter
|
formatter *text.Formatter
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
parseMention gtsmodel.ParseMentionFunc
|
parseMention gtsmodel.ParseMentionFunc
|
||||||
|
@ -52,7 +52,7 @@ func New(
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
mediaManager *media.Manager,
|
mediaManager *media.Manager,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
filter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
parseMention gtsmodel.ParseMentionFunc,
|
parseMention gtsmodel.ParseMentionFunc,
|
||||||
) Processor {
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
|
@ -60,7 +60,7 @@ func New(
|
||||||
state: state,
|
state: state,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
mediaManager: mediaManager,
|
mediaManager: mediaManager,
|
||||||
filter: filter,
|
visFilter: visFilter,
|
||||||
formatter: text.NewFormatter(state.DB),
|
formatter: text.NewFormatter(state.DB),
|
||||||
federator: federator,
|
federator: federator,
|
||||||
parseMention: parseMention,
|
parseMention: parseMention,
|
||||||
|
|
|
@ -64,7 +64,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode
|
||||||
return nil, gtserror.NewErrorInternalError(err) // A real error has occurred.
|
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 {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error checking bookmarked status visibility: %s", err)
|
log.Errorf(ctx, "error checking bookmarked status visibility: %s", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (p *Processor) ListsGet(ctx context.Context, requestingAccount *gtsmodel.Ac
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err))
|
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 {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err))
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,7 @@ func (p *Processor) StatusesGet(
|
||||||
|
|
||||||
// Filtering + serialization process is the same for
|
// Filtering + serialization process is the same for
|
||||||
// both pinned status queries and 'normal' ones.
|
// 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 {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
@ -114,6 +115,8 @@ func (suite *AdminStandardTestSuite) SetupTest() {
|
||||||
suite.mediaManager,
|
suite.mediaManager,
|
||||||
&suite.state,
|
&suite.state,
|
||||||
suite.emailSender,
|
suite.emailSender,
|
||||||
|
visibility.NewFilter(&suite.state),
|
||||||
|
interaction.NewFilter(&suite.state),
|
||||||
)
|
)
|
||||||
|
|
||||||
testrig.StartWorkers(&suite.state, suite.processor.Workers())
|
testrig.StartWorkers(&suite.state, suite.processor.Workers())
|
||||||
|
|
|
@ -55,7 +55,7 @@ func (p *Processor) GetTargetAccountBy(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether target account is visible to requesting account.
|
// 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 {
|
if err != nil {
|
||||||
return nil, false, gtserror.NewErrorInternalError(err)
|
return nil, false, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
@ -241,7 +241,7 @@ func (p *Processor) getVisibleAPIAccounts(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether this account is visible to requesting account.
|
// 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 {
|
if err != nil {
|
||||||
l.Errorf("error checking account visibility: %v", err)
|
l.Errorf("error checking account visibility: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -33,7 +33,7 @@ type Processor struct {
|
||||||
media *media.Manager
|
media *media.Manager
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
filter *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Processor instance.
|
// New returns a new Processor instance.
|
||||||
|
@ -42,13 +42,13 @@ func New(
|
||||||
media *media.Manager,
|
media *media.Manager,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
filter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
) Processor {
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
state: state,
|
state: state,
|
||||||
media: media,
|
media: media,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
federator: federator,
|
federator: federator,
|
||||||
filter: filter,
|
visFilter: visFilter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ func (p *Processor) GetTargetStatusBy(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether target status is visible to requesting account.
|
// 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 {
|
if err != nil {
|
||||||
return nil, false, gtserror.NewErrorInternalError(err)
|
return nil, false, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
filter *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a
|
// New returns a
|
||||||
|
@ -42,13 +42,13 @@ func New(
|
||||||
common *common.Processor,
|
common *common.Processor,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
filter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
) Processor {
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
c: common,
|
c: common,
|
||||||
state: state,
|
state: state,
|
||||||
federator: federator,
|
federator: federator,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
filter: filter,
|
visFilter: visFilter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
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 {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ func (p *Processor) StatusRepliesGet(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reslice replies dropping all those invisible to requester.
|
// 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 {
|
if err != nil {
|
||||||
err := gtserror.Newf("error filtering status replies: %w", err)
|
err := gtserror.Newf("error filtering status replies: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
mm "github.com/superseriousbusiness/gotosocial/internal/media"
|
mm "github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
@ -173,11 +174,10 @@ func NewProcessor(
|
||||||
mediaManager *mm.Manager,
|
mediaManager *mm.Manager,
|
||||||
state *state.State,
|
state *state.State,
|
||||||
emailSender email.Sender,
|
emailSender email.Sender,
|
||||||
|
visFilter *visibility.Filter,
|
||||||
|
intFilter *interaction.Filter,
|
||||||
) *Processor {
|
) *Processor {
|
||||||
var (
|
var parseMentionFunc = GetParseMentionFunc(state, federator)
|
||||||
parseMentionFunc = GetParseMentionFunc(state, federator)
|
|
||||||
filter = visibility.NewFilter(state)
|
|
||||||
)
|
|
||||||
|
|
||||||
processor := &Processor{
|
processor := &Processor{
|
||||||
converter: converter,
|
converter: converter,
|
||||||
|
@ -191,26 +191,26 @@ func NewProcessor(
|
||||||
//
|
//
|
||||||
// Start with sub processors that will
|
// Start with sub processors that will
|
||||||
// be required by the workers processor.
|
// be required by the workers processor.
|
||||||
common := common.New(state, mediaManager, converter, federator, filter)
|
common := common.New(state, mediaManager, converter, federator, visFilter)
|
||||||
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
|
processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc)
|
||||||
processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
|
processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
|
||||||
processor.stream = stream.New(state, oauthServer)
|
processor.stream = stream.New(state, oauthServer)
|
||||||
|
|
||||||
// Instantiate the rest of the sub
|
// Instantiate the rest of the sub
|
||||||
// processors + pin them to this struct.
|
// 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.admin = admin.New(&common, state, cleaner, federator, converter, mediaManager, federator.TransportController(), emailSender)
|
||||||
processor.conversations = conversations.New(state, converter, filter)
|
processor.conversations = conversations.New(state, converter, visFilter)
|
||||||
processor.fedi = fedi.New(state, &common, converter, federator, filter)
|
processor.fedi = fedi.New(state, &common, converter, federator, visFilter)
|
||||||
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
|
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
|
||||||
processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
|
processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
|
||||||
processor.list = list.New(state, converter)
|
processor.list = list.New(state, converter)
|
||||||
processor.markers = markers.New(state, converter)
|
processor.markers = markers.New(state, converter)
|
||||||
processor.polls = polls.New(&common, state, converter)
|
processor.polls = polls.New(&common, state, converter)
|
||||||
processor.report = report.New(state, converter)
|
processor.report = report.New(state, converter)
|
||||||
processor.timeline = timeline.New(state, converter, filter)
|
processor.timeline = timeline.New(state, converter, visFilter)
|
||||||
processor.search = search.New(state, federator, converter, filter)
|
processor.search = search.New(state, federator, converter, visFilter)
|
||||||
processor.status = status.New(state, &common, &processor.polls, federator, converter, filter, parseMentionFunc)
|
processor.status = status.New(state, &common, &processor.polls, federator, converter, visFilter, intFilter, parseMentionFunc)
|
||||||
processor.user = user.New(state, converter, oauthServer, emailSender)
|
processor.user = user.New(state, converter, oauthServer, emailSender)
|
||||||
|
|
||||||
// The advanced migrations processor sequences advanced migrations from all other processors.
|
// The advanced migrations processor sequences advanced migrations from all other processors.
|
||||||
|
@ -223,7 +223,7 @@ func NewProcessor(
|
||||||
state,
|
state,
|
||||||
federator,
|
federator,
|
||||||
converter,
|
converter,
|
||||||
filter,
|
visFilter,
|
||||||
emailSender,
|
emailSender,
|
||||||
&processor.account,
|
&processor.account,
|
||||||
&processor.media,
|
&processor.media,
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
@ -122,7 +123,17 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
|
||||||
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
||||||
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
|
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.StartWorkers(&suite.state, suite.processor.Workers())
|
||||||
|
|
||||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
|
|
|
@ -28,15 +28,15 @@ type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
filter *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new status processor.
|
// 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{
|
return Processor{
|
||||||
state: state,
|
state: state,
|
||||||
federator: federator,
|
federator: federator,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
filter: filter,
|
visFilter: visFilter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ func (p *Processor) packageStatuses(
|
||||||
|
|
||||||
for _, status := range statuses {
|
for _, status := range statuses {
|
||||||
// Ensure requester can see result status.
|
// 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 {
|
if err != nil {
|
||||||
err = gtserror.Newf("error checking visibility of status %s for account %s: %w", status.ID, requestingAccount.ID, err)
|
err = gtserror.Newf("error checking visibility of status %s for account %s: %w", status.ID, requestingAccount.ID, err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (p *Processor) BoostCreate(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure valid boost target for requester.
|
// Ensure valid boost target for requester.
|
||||||
boostable, err := p.filter.StatusBoostable(ctx,
|
policyResult, err := p.intFilter.StatusBoostable(ctx,
|
||||||
requester,
|
requester,
|
||||||
target,
|
target,
|
||||||
)
|
)
|
||||||
|
@ -75,12 +75,14 @@ func (p *Processor) BoostCreate(
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !boostable {
|
if policyResult.Forbidden() {
|
||||||
err := gtserror.New("status is not boostable")
|
const errText = "you do not have permission to boost this status"
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
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,
|
boost, err := p.converter.StatusToBoost(ctx,
|
||||||
target,
|
target,
|
||||||
requester,
|
requester,
|
||||||
|
@ -90,6 +92,29 @@ func (p *Processor) BoostCreate(
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
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.
|
// Store the new boost.
|
||||||
if err := p.state.DB.PutStatus(ctx, boost); err != nil {
|
if err := p.state.DB.PutStatus(ctx, boost); err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
@ -184,7 +209,7 @@ func (p *Processor) StatusBoostedBy(ctx context.Context, requestingAccount *gtsm
|
||||||
targetStatus = boostedStatus
|
targetStatus = boostedStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus)
|
visible, err := p.visFilter.StatusVisible(ctx, requestingAccount, targetStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)
|
err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
|
|
@ -341,7 +341,7 @@ func (p *Processor) ContextGet(
|
||||||
// Convert ancestors + filter
|
// Convert ancestors + filter
|
||||||
// out ones that aren't visible.
|
// out ones that aren't visible.
|
||||||
for _, status := range threadContext.ancestors {
|
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)
|
status, err := convert(ctx, status, requester)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
apiContext.Ancestors = append(apiContext.Ancestors, *status)
|
apiContext.Ancestors = append(apiContext.Ancestors, *status)
|
||||||
|
@ -352,7 +352,7 @@ func (p *Processor) ContextGet(
|
||||||
// Convert descendants + filter
|
// Convert descendants + filter
|
||||||
// out ones that aren't visible.
|
// out ones that aren't visible.
|
||||||
for _, status := range threadContext.descendants {
|
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)
|
status, err := convert(ctx, status, requester)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
apiContext.Descendants = append(apiContext.Descendants, *status)
|
apiContext.Descendants = append(apiContext.Descendants, *status)
|
||||||
|
@ -453,7 +453,7 @@ func (p *Processor) WebContextGet(
|
||||||
// Ensure status is actually
|
// Ensure status is actually
|
||||||
// visible to just anyone, and
|
// visible to just anyone, and
|
||||||
// hide / don't include it if not.
|
// 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 err != nil || !v {
|
||||||
if !inReplies {
|
if !inReplies {
|
||||||
// Main thread entry hidden.
|
// Main thread entry hidden.
|
||||||
|
|
|
@ -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 {
|
func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, inReplyToID string) gtserror.WithCode {
|
||||||
if inReplyToID == "" {
|
if inReplyToID == "" {
|
||||||
|
// Not a reply.
|
||||||
|
// Nothing to do.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,6 +193,45 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac
|
||||||
return errWithCode
|
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.
|
// Set status fields from inReplyTo.
|
||||||
status.InReplyToID = inReplyTo.ID
|
status.InReplyToID = inReplyTo.ID
|
||||||
status.InReplyTo = inReplyTo
|
status.InReplyTo = inReplyTo
|
||||||
|
|
|
@ -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).
|
// 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) {
|
func (p *Processor) FaveCreate(
|
||||||
targetStatus, existingFave, errWithCode := p.getFaveableStatus(ctx, requestingAccount, targetStatusID)
|
ctx context.Context,
|
||||||
|
requester *gtsmodel.Account,
|
||||||
|
targetStatusID string,
|
||||||
|
) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
status, existingFave, errWithCode := p.getFaveableStatus(ctx, requester, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingFave != nil {
|
if existingFave != nil {
|
||||||
// Status is already faveed.
|
// 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()
|
faveID := id.NewULID()
|
||||||
gtsFave := >smodel.StatusFave{
|
gtsFave := >smodel.StatusFave{
|
||||||
ID: faveID,
|
ID: faveID,
|
||||||
AccountID: requestingAccount.ID,
|
AccountID: requester.ID,
|
||||||
Account: requestingAccount,
|
Account: requester,
|
||||||
TargetAccountID: targetStatus.AccountID,
|
TargetAccountID: status.AccountID,
|
||||||
TargetAccount: targetStatus.Account,
|
TargetAccount: status.Account,
|
||||||
StatusID: targetStatus.ID,
|
StatusID: status.ID,
|
||||||
Status: targetStatus,
|
Status: status,
|
||||||
URI: uris.GenerateURIForLike(requestingAccount.Username, faveID),
|
URI: uris.GenerateURIForLike(requester.Username, faveID),
|
||||||
|
PendingApproval: &pendingApproval,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil {
|
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,
|
APObjectType: ap.ActivityLike,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: gtsFave,
|
GTSModel: gtsFave,
|
||||||
Origin: requestingAccount,
|
Origin: requester,
|
||||||
Target: targetStatus.Account,
|
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).
|
// FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist).
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
|
@ -35,7 +36,8 @@ type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
filter *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
|
intFilter *interaction.Filter
|
||||||
formatter *text.Formatter
|
formatter *text.Formatter
|
||||||
parseMention gtsmodel.ParseMentionFunc
|
parseMention gtsmodel.ParseMentionFunc
|
||||||
|
|
||||||
|
@ -50,7 +52,8 @@ func New(
|
||||||
polls *polls.Processor,
|
polls *polls.Processor,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
filter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
|
intFilter *interaction.Filter,
|
||||||
parseMention gtsmodel.ParseMentionFunc,
|
parseMention gtsmodel.ParseMentionFunc,
|
||||||
) Processor {
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
|
@ -58,7 +61,8 @@ func New(
|
||||||
state: state,
|
state: state,
|
||||||
federator: federator,
|
federator: federator,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
filter: filter,
|
visFilter: visFilter,
|
||||||
|
intFilter: intFilter,
|
||||||
formatter: text.NewFormatter(state.DB),
|
formatter: text.NewFormatter(state.DB),
|
||||||
parseMention: parseMention,
|
parseMention: parseMention,
|
||||||
polls: polls,
|
polls: polls,
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
@ -89,16 +90,30 @@ func (suite *StatusStandardTestSuite) SetupTest() {
|
||||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||||
suite.federator = testrig.NewTestFederator(&suite.state, suite.tc, suite.mediaManager)
|
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(
|
testrig.StartTimelines(
|
||||||
&suite.state,
|
&suite.state,
|
||||||
filter,
|
visFilter,
|
||||||
suite.typeConverter,
|
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)
|
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.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||||
|
|
|
@ -133,19 +133,22 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma
|
||||||
|
|
||||||
items := make([]interface{}, 0, count)
|
items := make([]interface{}, 0, count)
|
||||||
for _, s := range statuses {
|
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 {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error checking status visibility: %v", err)
|
log.Errorf(ctx, "error checking status visibility: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -62,7 +62,7 @@ func HomeTimelineGrab(state *state.State) timeline.GrabFunction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HomeTimelineFilter returns a function that satisfies FilterFunction for home timelines.
|
// 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) {
|
return func(ctx context.Context, accountID string, item timeline.Timelineable) (shouldIndex bool, err error) {
|
||||||
status, ok := item.(*gtsmodel.Status)
|
status, ok := item.(*gtsmodel.Status)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -76,7 +76,7 @@ func HomeTimelineFilter(state *state.State, filter *visibility.Filter) timeline.
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
timelineable, err := filter.StatusHomeTimelineable(ctx, requestingAccount, status)
|
timelineable, err := visFilter.StatusHomeTimelineable(ctx, requestingAccount, status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, accountID, err)
|
err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, accountID, err)
|
||||||
return false, err
|
return false, err
|
||||||
|
|
|
@ -62,7 +62,7 @@ func ListTimelineGrab(state *state.State) timeline.GrabFunction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListTimelineFilter returns a function that satisfies FilterFunction for list timelines.
|
// 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) {
|
return func(ctx context.Context, listID string, item timeline.Timelineable) (shouldIndex bool, err error) {
|
||||||
status, ok := item.(*gtsmodel.Status)
|
status, ok := item.(*gtsmodel.Status)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -82,7 +82,7 @@ func ListTimelineFilter(state *state.State, filter *visibility.Filter) timeline.
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
timelineable, err := filter.StatusHomeTimelineable(ctx, requestingAccount, status)
|
timelineable, err := visFilter.StatusHomeTimelineable(ctx, requestingAccount, status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, list.AccountID, err)
|
err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, list.AccountID, err)
|
||||||
return false, err
|
return false, err
|
||||||
|
|
|
@ -190,7 +190,7 @@ func (p *Processor) notifVisible(
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.filter.AccountVisible(ctx, acct, n.OriginAccount)
|
visible, err := p.visFilter.AccountVisible(ctx, acct, n.OriginAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ func (p *Processor) notifVisible(
|
||||||
// If status is set, ensure it's
|
// If status is set, ensure it's
|
||||||
// visible to notif target.
|
// visible to notif target.
|
||||||
if n.Status != nil {
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ func (p *Processor) PublicTimelineGet(
|
||||||
// we end up filtering it out or not.
|
// we end up filtering it out or not.
|
||||||
nextMaxIDValue = s.ID
|
nextMaxIDValue = s.ID
|
||||||
|
|
||||||
timelineable, err := p.filter.StatusPublicTimelineable(ctx, requester, s)
|
timelineable, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error checking status visibility: %v", err)
|
log.Errorf(ctx, "error checking status visibility: %v", err)
|
||||||
continue inner
|
continue inner
|
||||||
|
|
|
@ -128,7 +128,7 @@ func (p *Processor) packageTagResponse(
|
||||||
compiledMutes := usermute.NewCompiledUserMuteList(mutes)
|
compiledMutes := usermute.NewCompiledUserMuteList(mutes)
|
||||||
|
|
||||||
for _, s := range statuses {
|
for _, s := range statuses {
|
||||||
timelineable, err := p.filter.StatusTagTimelineable(ctx, requestingAcct, s)
|
timelineable, err := p.visFilter.StatusTagTimelineable(ctx, requestingAcct, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error checking status visibility: %v", err)
|
log.Errorf(ctx, "error checking status visibility: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -26,13 +26,13 @@
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
converter *typeutils.Converter
|
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{
|
return Processor{
|
||||||
state: state,
|
state: state,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
filter: filter,
|
visFilter: visFilter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,8 +45,12 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := suite.SetupTestStructs()
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer suite.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
boostedStatus := suite.testStatuses["local_account_1_status_1"]
|
boostedStatus := >smodel.Status{}
|
||||||
boostingAccount := suite.testAccounts["remote_account_1"]
|
*boostedStatus = *suite.testStatuses["local_account_1_status_1"]
|
||||||
|
|
||||||
|
boostingAccount := >smodel.Account{}
|
||||||
|
*boostingAccount = *suite.testAccounts["remote_account_1"]
|
||||||
|
|
||||||
announceStatus := >smodel.Status{}
|
announceStatus := >smodel.Status{}
|
||||||
announceStatus.URI = "https://example.org/some-announce-uri"
|
announceStatus.URI = "https://example.org/some-announce-uri"
|
||||||
announceStatus.BoostOfURI = boostedStatus.URI
|
announceStatus.BoostOfURI = boostedStatus.URI
|
||||||
|
@ -64,13 +68,25 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
||||||
Receiving: suite.testAccounts["local_account_1"],
|
Receiving: suite.testAccounts["local_account_1"],
|
||||||
Requesting: boostingAccount,
|
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
|
// 1. status should have an ID, and be in the database
|
||||||
suite.NotEmpty(announceStatus.ID)
|
if !testrig.WaitFor(func() bool {
|
||||||
_, err = testStructs.State.DB.GetStatusByID(context.Background(), announceStatus.ID)
|
if announceStatus.ID == "" {
|
||||||
suite.NoError(err)
|
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
|
// 2. a notification should exist for the announce
|
||||||
where := []db.Where{
|
where := []db.Where{
|
||||||
|
@ -89,78 +105,89 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
||||||
suite.False(*notif.Read)
|
suite.False(*notif.Read)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: fix this test up in interaction policies PR.
|
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||||
// func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
testStructs := suite.SetupTestStructs()
|
||||||
// testStructs := suite.SetupTestStructs()
|
defer suite.TearDownTestStructs(testStructs)
|
||||||
// defer suite.TearDownTestStructs(testStructs)
|
|
||||||
|
|
||||||
// repliedAccount := suite.testAccounts["local_account_1"]
|
repliedAccount := >smodel.Account{}
|
||||||
// repliedStatus := suite.testStatuses["local_account_1_status_1"]
|
*repliedAccount = *suite.testAccounts["local_account_1"]
|
||||||
// replyingAccount := suite.testAccounts["remote_account_1"]
|
|
||||||
|
|
||||||
// // Set the replyingAccount's last fetched_at
|
repliedStatus := >smodel.Status{}
|
||||||
// // date to something recent so no refresh is attempted,
|
*repliedStatus = *suite.testStatuses["local_account_1_status_1"]
|
||||||
// // 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)
|
|
||||||
|
|
||||||
// // Get replying statusable to use from remote test statuses.
|
replyingAccount := >smodel.Account{}
|
||||||
// const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552"
|
*replyingAccount = *suite.testAccounts["remote_account_1"]
|
||||||
// replyingStatusable := testrig.NewTestFediStatuses()[replyingURI]
|
|
||||||
// ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI))
|
|
||||||
|
|
||||||
// // Open a websocket stream to later test the streamed status reply.
|
// Set the replyingAccount's last fetched_at
|
||||||
// wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
|
// date to something recent so no refresh is attempted,
|
||||||
// suite.NoError(errWithCode)
|
// 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.
|
// Get replying statusable to use from remote test statuses.
|
||||||
// err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
|
const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552"
|
||||||
// APObjectType: ap.ObjectNote,
|
replyingStatusable := testrig.NewTestFediStatuses()[replyingURI]
|
||||||
// APActivityType: ap.ActivityCreate,
|
ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI))
|
||||||
// APObject: replyingStatusable,
|
|
||||||
// Receiving: repliedAccount,
|
|
||||||
// Requesting: replyingAccount,
|
|
||||||
// })
|
|
||||||
// suite.NoError(err)
|
|
||||||
|
|
||||||
// // side effects should be triggered
|
// Open a websocket stream to later test the streamed status reply.
|
||||||
// // 1. status should be in the database
|
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
|
||||||
// replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI)
|
suite.NoError(errWithCode)
|
||||||
// suite.NoError(err)
|
|
||||||
|
|
||||||
// // 2. a notification should exist for the mention
|
// Send the replied status off to the fedi worker to be further processed.
|
||||||
// var notif gtsmodel.Notification
|
err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
|
||||||
// err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{
|
APObjectType: ap.ObjectNote,
|
||||||
// {Key: "status_id", Value: replyingStatus.ID},
|
APActivityType: ap.ActivityCreate,
|
||||||
// }, ¬if)
|
APObject: replyingStatusable,
|
||||||
// suite.NoError(err)
|
Receiving: repliedAccount,
|
||||||
// suite.Equal(gtsmodel.NotificationMention, notif.NotificationType)
|
Requesting: replyingAccount,
|
||||||
// suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID)
|
})
|
||||||
// suite.Equal(replyingStatus.AccountID, notif.OriginAccountID)
|
if err != nil {
|
||||||
// suite.Equal(replyingStatus.ID, notif.StatusID)
|
suite.FailNow(err.Error())
|
||||||
// suite.False(*notif.Read)
|
}
|
||||||
|
|
||||||
// ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
|
// Wait for side effects to trigger:
|
||||||
// msg, ok := wssStream.Recv(ctx)
|
// 1. status should be in the database
|
||||||
// suite.True(ok)
|
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)
|
// 2. a notification should exist for the mention
|
||||||
// suite.NotEmpty(msg.Payload)
|
var notif gtsmodel.Notification
|
||||||
// suite.EqualValues([]string{stream.TimelineHome}, msg.Stream)
|
err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{
|
||||||
// notifStreamed := &apimodel.Notification{}
|
{Key: "status_id", Value: replyingStatus.ID},
|
||||||
// err = json.Unmarshal([]byte(msg.Payload), notifStreamed)
|
}, ¬if)
|
||||||
// suite.NoError(err)
|
suite.NoError(err)
|
||||||
// suite.Equal("mention", notifStreamed.Type)
|
suite.Equal(gtsmodel.NotificationMention, notif.NotificationType)
|
||||||
// suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
|
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() {
|
func (suite *FromFediAPITestSuite) TestProcessFave() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := suite.SetupTestStructs()
|
||||||
|
@ -305,8 +332,11 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
deletedAccount := suite.testAccounts["remote_account_1"]
|
deletedAccount := >smodel.Account{}
|
||||||
receivingAccount := suite.testAccounts["local_account_1"]
|
*deletedAccount = *suite.testAccounts["remote_account_1"]
|
||||||
|
|
||||||
|
receivingAccount := >smodel.Account{}
|
||||||
|
*receivingAccount = *suite.testAccounts["local_account_1"]
|
||||||
|
|
||||||
// before doing the delete....
|
// before doing the delete....
|
||||||
// make local_account_1 and remote_account_1 into mufos
|
// make local_account_1 and remote_account_1 into mufos
|
||||||
|
|
|
@ -36,7 +36,7 @@ type Surface struct {
|
||||||
State *state.State
|
State *state.State
|
||||||
Converter *typeutils.Converter
|
Converter *typeutils.Converter
|
||||||
Stream *stream.Processor
|
Stream *stream.Processor
|
||||||
Filter *visibility.Filter
|
VisFilter *visibility.Filter
|
||||||
EmailSender email.Sender
|
EmailSender email.Sender
|
||||||
Conversations *conversations.Processor
|
Conversations *conversations.Processor
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
|
||||||
State: testStructs.State,
|
State: testStructs.State,
|
||||||
Converter: testStructs.TypeConverter,
|
Converter: testStructs.TypeConverter,
|
||||||
Stream: testStructs.Processor.Stream(),
|
Stream: testStructs.Processor.Stream(),
|
||||||
Filter: visibility.NewFilter(testStructs.State),
|
VisFilter: visibility.NewFilter(testStructs.State),
|
||||||
EmailSender: testStructs.EmailSender,
|
EmailSender: testStructs.EmailSender,
|
||||||
Conversations: testStructs.Processor.Conversations(),
|
Conversations: testStructs.Processor.Conversations(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
|
||||||
// If it's not timelineable, we can just stop early, since lists
|
// If it's not timelineable, we can just stop early, since lists
|
||||||
// are prettymuch subsets of the home timeline, so if it shouldn't
|
// are prettymuch subsets of the home timeline, so if it shouldn't
|
||||||
// appear there, it shouldn't appear in lists either.
|
// appear there, it shouldn't appear in lists either.
|
||||||
timelineable, err := s.Filter.StatusHomeTimelineable(
|
timelineable, err := s.VisFilter.StatusHomeTimelineable(
|
||||||
ctx, follow.Account, status,
|
ctx, follow.Account, status,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -482,7 +482,7 @@ func (s *Surface) timelineStatusUpdateForFollowers(
|
||||||
// If it's not timelineable, we can just stop early, since lists
|
// If it's not timelineable, we can just stop early, since lists
|
||||||
// are prettymuch subsets of the home timeline, so if it shouldn't
|
// are prettymuch subsets of the home timeline, so if it shouldn't
|
||||||
// appear there, it shouldn't appear in lists either.
|
// appear there, it shouldn't appear in lists either.
|
||||||
timelineable, err := s.Filter.StatusHomeTimelineable(
|
timelineable, err := s.VisFilter.StatusHomeTimelineable(
|
||||||
ctx, follow.Account, status,
|
ctx, follow.Account, status,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -40,7 +40,7 @@ func New(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
filter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
emailSender email.Sender,
|
emailSender email.Sender,
|
||||||
account *account.Processor,
|
account *account.Processor,
|
||||||
media *media.Processor,
|
media *media.Processor,
|
||||||
|
@ -61,7 +61,7 @@ func New(
|
||||||
State: state,
|
State: state,
|
||||||
Converter: converter,
|
Converter: converter,
|
||||||
Stream: stream,
|
Stream: stream,
|
||||||
Filter: filter,
|
VisFilter: visFilter,
|
||||||
EmailSender: emailSender,
|
EmailSender: emailSender,
|
||||||
Conversations: conversations,
|
Conversations: conversations,
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
@ -160,7 +161,18 @@ func (suite *WorkersTestSuite) SetupTestStructs() *TestStructs {
|
||||||
oauthServer := testrig.NewTestOauthServer(db)
|
oauthServer := testrig.NewTestOauthServer(db)
|
||||||
emailSender := testrig.NewEmailSender("../../../web/template/", nil)
|
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.StartWorkers(&state, processor.Workers())
|
||||||
|
|
||||||
testrig.StandardDBSetup(db, suite.testAccounts)
|
testrig.StandardDBSetup(db, suite.testAccounts)
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
)
|
)
|
||||||
|
@ -28,13 +29,15 @@ type Converter struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
defaultAvatars []string
|
defaultAvatars []string
|
||||||
randAvatars sync.Map
|
randAvatars sync.Map
|
||||||
filter *visibility.Filter
|
visFilter *visibility.Filter
|
||||||
|
intFilter *interaction.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConverter(state *state.State) *Converter {
|
func NewConverter(state *state.State) *Converter {
|
||||||
return &Converter{
|
return &Converter{
|
||||||
state: state,
|
state: state,
|
||||||
defaultAvatars: populateDefaultAvatars(),
|
defaultAvatars: populateDefaultAvatars(),
|
||||||
filter: visibility.NewFilter(state),
|
visFilter: visibility.NewFilter(state),
|
||||||
|
intFilter: interaction.NewFilter(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -861,7 +861,7 @@ func (c *Converter) statusToAPIFilterResults(
|
||||||
|
|
||||||
for _, account := range otherAccounts {
|
for _, account := range otherAccounts {
|
||||||
// Is this account visible?
|
// Is this account visible?
|
||||||
visible, err := c.filter.AccountVisible(ctx, requestingAccount, account)
|
visible, err := c.visFilter.AccountVisible(ctx, requestingAccount, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -2382,8 +2382,8 @@ func (c *Converter) ThemesToAPIThemes(themes []*gtsmodel.Theme) []apimodel.Theme
|
||||||
func (c *Converter) InteractionPolicyToAPIInteractionPolicy(
|
func (c *Converter) InteractionPolicyToAPIInteractionPolicy(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
policy *gtsmodel.InteractionPolicy,
|
policy *gtsmodel.InteractionPolicy,
|
||||||
_ *gtsmodel.Status, // Used in upcoming PR.
|
status *gtsmodel.Status,
|
||||||
_ *gtsmodel.Account, // Used in upcoming PR.
|
requester *gtsmodel.Account,
|
||||||
) (*apimodel.InteractionPolicy, error) {
|
) (*apimodel.InteractionPolicy, error) {
|
||||||
apiPolicy := &apimodel.InteractionPolicy{
|
apiPolicy := &apimodel.InteractionPolicy{
|
||||||
CanFavourite: apimodel.PolicyRules{
|
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
|
return apiPolicy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -551,19 +551,22 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -747,19 +750,22 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -927,19 +933,22 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -1010,19 +1019,22 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -1257,19 +1269,22 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -1560,19 +1575,22 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage()
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
@ -2561,19 +2579,22 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
||||||
"interaction_policy": {
|
"interaction_policy": {
|
||||||
"can_favourite": {
|
"can_favourite": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reply": {
|
"can_reply": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
},
|
},
|
||||||
"can_reblog": {
|
"can_reblog": {
|
||||||
"always": [
|
"always": [
|
||||||
"public"
|
"public",
|
||||||
|
"me"
|
||||||
],
|
],
|
||||||
"with_approval": []
|
"with_approval": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -28,5 +29,13 @@
|
||||||
|
|
||||||
// NewTestFederator returns a federator with the given database and (mock!!) transport controller.
|
// 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 {
|
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"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/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -31,5 +33,15 @@
|
||||||
// The passed in state will have its worker functions set appropriately,
|
// The passed in state will have its worker functions set appropriately,
|
||||||
// but the state will not be initialized.
|
// but the state will not be initialized.
|
||||||
func NewTestProcessor(state *state.State, federator *federation.Federator, emailSender email.Sender, mediaManager *media.Manager) *processing.Processor {
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1436,6 +1436,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"admin_account_status_2": {
|
"admin_account_status_2": {
|
||||||
ID: "01F8MHAAY43M6RJ473VQFCVH37",
|
ID: "01F8MHAAY43M6RJ473VQFCVH37",
|
||||||
|
@ -1459,6 +1460,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"admin_account_status_3": {
|
"admin_account_status_3": {
|
||||||
ID: "01FF25D5Q0DH7CHD57CTRS6WK0",
|
ID: "01FF25D5Q0DH7CHD57CTRS6WK0",
|
||||||
|
@ -1483,6 +1485,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"admin_account_status_4": {
|
"admin_account_status_4": {
|
||||||
ID: "01G36SF3V6Y6V5BF9P4R7PQG7G",
|
ID: "01G36SF3V6Y6V5BF9P4R7PQG7G",
|
||||||
|
@ -1504,6 +1507,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_1": {
|
"local_account_1_status_1": {
|
||||||
ID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
ID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
@ -1526,6 +1530,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_2": {
|
"local_account_1_status_2": {
|
||||||
ID: "01F8MHAYFKS4KMXF8K5Y1C0KRN",
|
ID: "01F8MHAYFKS4KMXF8K5Y1C0KRN",
|
||||||
|
@ -1548,6 +1553,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
Federated: util.Ptr(false),
|
Federated: util.Ptr(false),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_3": {
|
"local_account_1_status_3": {
|
||||||
ID: "01F8MHBBN8120SYH7D5S050MGK",
|
ID: "01F8MHBBN8120SYH7D5S050MGK",
|
||||||
|
@ -1581,6 +1587,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_4": {
|
"local_account_1_status_4": {
|
||||||
ID: "01F8MH82FYRXD2RC6108DAJ5HB",
|
ID: "01F8MH82FYRXD2RC6108DAJ5HB",
|
||||||
|
@ -1604,6 +1611,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_5": {
|
"local_account_1_status_5": {
|
||||||
ID: "01FCTA44PW9H1TB328S9AQXKDS",
|
ID: "01FCTA44PW9H1TB328S9AQXKDS",
|
||||||
|
@ -1627,6 +1635,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_6": {
|
"local_account_1_status_6": {
|
||||||
ID: "01HEN2RZ8BG29Y5Z9VJC73HZW7",
|
ID: "01HEN2RZ8BG29Y5Z9VJC73HZW7",
|
||||||
|
@ -1651,6 +1660,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ActivityQuestion,
|
ActivityStreamsType: ap.ActivityQuestion,
|
||||||
PollID: "01HEN2RKT1YTEZ80SA8HGP105F",
|
PollID: "01HEN2RKT1YTEZ80SA8HGP105F",
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_7": {
|
"local_account_1_status_7": {
|
||||||
ID: "01HH9KYNQPA416TNJ53NSATP40",
|
ID: "01HH9KYNQPA416TNJ53NSATP40",
|
||||||
|
@ -1673,6 +1683,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_1_status_8": {
|
"local_account_1_status_8": {
|
||||||
ID: "01J2M1HPFSS54S60Y0KYV23KJE",
|
ID: "01J2M1HPFSS54S60Y0KYV23KJE",
|
||||||
|
@ -1720,6 +1731,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_2_status_2": {
|
"local_account_2_status_2": {
|
||||||
ID: "01F8MHC0H0A7XHTVH5F596ZKBM",
|
ID: "01F8MHC0H0A7XHTVH5F596ZKBM",
|
||||||
|
@ -1753,6 +1765,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_2_status_3": {
|
"local_account_2_status_3": {
|
||||||
ID: "01F8MHC8VWDRBQR0N1BATDDEM5",
|
ID: "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
@ -1787,6 +1800,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_2_status_4": {
|
"local_account_2_status_4": {
|
||||||
ID: "01F8MHCP5P2NWYQ416SBA0XSEV",
|
ID: "01F8MHCP5P2NWYQ416SBA0XSEV",
|
||||||
|
@ -1820,6 +1834,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_2_status_5": {
|
"local_account_2_status_5": {
|
||||||
ID: "01FCQSQ667XHJ9AV9T27SJJSX5",
|
ID: "01FCQSQ667XHJ9AV9T27SJJSX5",
|
||||||
|
@ -1845,6 +1860,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_2_status_6": {
|
"local_account_2_status_6": {
|
||||||
ID: "01FN3VJGFH10KR7S2PB0GFJZYG",
|
ID: "01FN3VJGFH10KR7S2PB0GFJZYG",
|
||||||
|
@ -1870,6 +1886,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_2_status_7": {
|
"local_account_2_status_7": {
|
||||||
ID: "01G20ZM733MGN8J344T4ZDDFY1",
|
ID: "01G20ZM733MGN8J344T4ZDDFY1",
|
||||||
|
@ -1894,6 +1911,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"local_account_2_status_8": {
|
"local_account_2_status_8": {
|
||||||
ID: "01HEN2PRXT0TF4YDRA64FZZRN7",
|
ID: "01HEN2PRXT0TF4YDRA64FZZRN7",
|
||||||
|
@ -1918,6 +1936,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ActivityQuestion,
|
ActivityStreamsType: ap.ActivityQuestion,
|
||||||
PollID: "01HEN2QB5NR4NCEHGYC3HN84K6",
|
PollID: "01HEN2QB5NR4NCEHGYC3HN84K6",
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"remote_account_1_status_1": {
|
"remote_account_1_status_1": {
|
||||||
ID: "01FVW7JHQFSFK166WWKR8CBA6M",
|
ID: "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||||
|
@ -1942,6 +1961,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "",
|
CreatedWithApplicationID: "",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"remote_account_1_status_2": {
|
"remote_account_1_status_2": {
|
||||||
ID: "01HEN2QRFA8H3C6QPN7RD4KSR6",
|
ID: "01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||||
|
@ -1966,6 +1986,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ActivityQuestion,
|
ActivityStreamsType: ap.ActivityQuestion,
|
||||||
PollID: "01HEN2R65468ZG657C4ZPHJ4EX",
|
PollID: "01HEN2R65468ZG657C4ZPHJ4EX",
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"remote_account_1_status_3": {
|
"remote_account_1_status_3": {
|
||||||
ID: "01HEWV37MHV8BAC8ANFGVRRM5D",
|
ID: "01HEWV37MHV8BAC8ANFGVRRM5D",
|
||||||
|
@ -1990,6 +2011,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ActivityQuestion,
|
ActivityStreamsType: ap.ActivityQuestion,
|
||||||
PollID: "01HEWV1GW2D49R919NPEDXPTZ5",
|
PollID: "01HEWV1GW2D49R919NPEDXPTZ5",
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
"remote_account_2_status_1": {
|
"remote_account_2_status_1": {
|
||||||
ID: "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
ID: "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
||||||
|
@ -2014,6 +2036,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
CreatedWithApplicationID: "",
|
CreatedWithApplicationID: "",
|
||||||
Federated: util.Ptr(true),
|
Federated: util.Ptr(true),
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,10 +91,10 @@ func StopWorkers(state *state.State) {
|
||||||
state.Workers.Dereference.Stop()
|
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(
|
state.Timelines.Home = timeline.NewManager(
|
||||||
tlprocessor.HomeTimelineGrab(state),
|
tlprocessor.HomeTimelineGrab(state),
|
||||||
tlprocessor.HomeTimelineFilter(state, filter),
|
tlprocessor.HomeTimelineFilter(state, visFilter),
|
||||||
tlprocessor.HomeTimelineStatusPrepare(state, converter),
|
tlprocessor.HomeTimelineStatusPrepare(state, converter),
|
||||||
tlprocessor.SkipInsert(),
|
tlprocessor.SkipInsert(),
|
||||||
)
|
)
|
||||||
|
@ -104,7 +104,7 @@ func StartTimelines(state *state.State, filter *visibility.Filter, converter *ty
|
||||||
|
|
||||||
state.Timelines.List = timeline.NewManager(
|
state.Timelines.List = timeline.NewManager(
|
||||||
tlprocessor.ListTimelineGrab(state),
|
tlprocessor.ListTimelineGrab(state),
|
||||||
tlprocessor.ListTimelineFilter(state, filter),
|
tlprocessor.ListTimelineFilter(state, visFilter),
|
||||||
tlprocessor.ListTimelineStatusPrepare(state, converter),
|
tlprocessor.ListTimelineStatusPrepare(state, converter),
|
||||||
tlprocessor.SkipInsert(),
|
tlprocessor.SkipInsert(),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue