From 5bc567196bf2204272950c525e8592e56057c3bd Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:44:29 +0200 Subject: [PATCH] [chore] Add interaction policy gtsmodels (#3075) * [chore] introduce interaction policy gts models * update migration a smidge * fix copy paste typo * update migration * use int for InteractionType --- docs/api/swagger.yaml | 15 - .../api/client/statuses/statusboost_test.go | 61 ++-- internal/api/client/statuses/statuscreate.go | 18 - .../api/client/statuses/statuscreate_test.go | 3 - .../api/client/statuses/statusfave_test.go | 61 ++-- .../api/client/statuses/statuspin_test.go | 3 - internal/api/model/status.go | 6 - internal/cache/cache.go | 2 + internal/cache/db.go | 37 +++ internal/cache/size.go | 17 +- internal/config/config.go | 93 +++--- internal/config/defaults.go | 91 ++--- internal/config/helpers.gen.go | 27 ++ internal/db/bundb/account_test.go | 9 - internal/db/bundb/bundb.go | 5 + internal/db/bundb/interaction.go | 149 +++++++++ .../20240620074530_interaction_policy.go | 264 +++++++++++++++ .../status.go | 61 ++++ internal/db/bundb/status_test.go | 18 - internal/db/bundb/statusfave.go | 36 ++ internal/db/bundb/timeline_test.go | 4 +- internal/db/db.go | 1 + internal/db/interaction.go | 41 +++ internal/db/statusfave.go | 8 +- internal/federation/dereferencing/announce.go | 3 - internal/federation/dereferencing/status.go | 4 +- .../federation/dereferencing/status_test.go | 12 - internal/filter/visibility/boostable.go | 5 - .../filter/visibility/home_timeline_test.go | 21 -- internal/gtsmodel/accountsettings.go | 27 +- internal/gtsmodel/interactionapproval.go | 55 +++ internal/gtsmodel/interactionpolicy.go | 314 ++++++++++++++++++ internal/gtsmodel/notification.go | 19 +- internal/gtsmodel/status.go | 6 +- internal/gtsmodel/statusfave.go | 2 + internal/processing/status/create.go | 57 +--- internal/processing/status/create_test.go | 21 -- internal/processing/status/fave.go | 5 - .../processing/workers/fromclientapi_test.go | 3 - .../processing/workers/fromfediapi_test.go | 125 +++---- internal/typeutils/astointernal.go | 3 - internal/typeutils/astointernal_test.go | 3 - internal/typeutils/internal.go | 3 - internal/typeutils/internaltorss_test.go | 3 - test/envparsing.sh | 1 + testrig/testmodels.go | 127 +++---- 46 files changed, 1318 insertions(+), 531 deletions(-) create mode 100644 internal/db/bundb/interaction.go create mode 100644 internal/db/bundb/migrations/20240620074530_interaction_policy.go create mode 100644 internal/db/bundb/migrations/20240620074530_interaction_policy/status.go create mode 100644 internal/db/interaction.go create mode 100644 internal/gtsmodel/interactionapproval.go create mode 100644 internal/gtsmodel/interactionpolicy.go diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 4ce234374..f7ce844af 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -8017,21 +8017,6 @@ paths: name: federated type: boolean x-go-name: Federated - - description: This status can be boosted/reblogged. - in: formData - name: boostable - type: boolean - x-go-name: Boostable - - description: This status can be replied to. - in: formData - name: replyable - type: boolean - x-go-name: Replyable - - description: This status can be liked/faved. - in: formData - name: likeable - type: boolean - x-go-name: Likeable produces: - application/json responses: diff --git a/internal/api/client/statuses/statusboost_test.go b/internal/api/client/statuses/statusboost_test.go index ae7c364bf..3e6a5853d 100644 --- a/internal/api/client/statuses/statusboost_test.go +++ b/internal/api/client/statuses/statusboost_test.go @@ -173,42 +173,43 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() { } // try to boost a status that's not boostable / visible to us -func (suite *StatusBoostTestSuite) TestPostUnboostable() { - t := suite.testTokens["local_account_1"] - oauthToken := oauth.DBTokenToToken(t) +// TODO: sort this out with new interaction policies +// func (suite *StatusBoostTestSuite) TestPostUnboostable() { +// t := suite.testTokens["local_account_1"] +// oauthToken := oauth.DBTokenToToken(t) - targetStatus := suite.testStatuses["local_account_2_status_4"] +// targetStatus := suite.testStatuses["local_account_2_status_4"] - // setup - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) - ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") +// // setup +// recorder := httptest.NewRecorder() +// ctx, _ := testrig.CreateGinTestContext(recorder, nil) +// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) +// ctx.Set(oauth.SessionAuthorizedToken, oauthToken) +// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) +// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) +// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting +// ctx.Request.Header.Set("accept", "application/json") - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: statuses.IDKey, - Value: targetStatus.ID, - }, - } +// // normally the router would populate these params from the path values, +// // but because we're calling the function directly, we need to set them manually. +// ctx.Params = gin.Params{ +// gin.Param{ +// Key: statuses.IDKey, +// Value: targetStatus.ID, +// }, +// } - suite.statusModule.StatusBoostPOSTHandler(ctx) +// suite.statusModule.StatusBoostPOSTHandler(ctx) - // check response - suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses +// // check response +// suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Equal(`{"error":"Not Found"}`, string(b)) -} +// result := recorder.Result() +// defer result.Body.Close() +// b, err := ioutil.ReadAll(result.Body) +// suite.NoError(err) +// suite.Equal(`{"error":"Not Found"}`, string(b)) +// } // try to boost a status that's not visible to the user func (suite *StatusBoostTestSuite) TestPostNotVisible() { diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go index 5a9654195..7b30e0ee6 100644 --- a/internal/api/client/statuses/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -168,24 +168,6 @@ import ( // description: This status will be federated beyond the local timeline(s). // in: formData // type: boolean -// - -// name: boostable -// x-go-name: Boostable -// description: This status can be boosted/reblogged. -// in: formData -// type: boolean -// - -// name: replyable -// x-go-name: Replyable -// description: This status can be replied to. -// in: formData -// type: boolean -// - -// name: likeable -// x-go-name: Likeable -// description: This status can be liked/faved. -// in: formData -// type: boolean // // produces: // - application/json diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go index b49e72ead..94d91c8b9 100644 --- a/internal/api/client/statuses/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -67,9 +67,6 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { "spoiler_text": {"hello hello"}, "sensitive": {"true"}, "visibility": {string(apimodel.VisibilityMutualsOnly)}, - "likeable": {"false"}, - "replyable": {"false"}, - "federated": {"false"}, } suite.statusModule.StatusCreatePOSTHandler(ctx) diff --git a/internal/api/client/statuses/statusfave_test.go b/internal/api/client/statuses/statusfave_test.go index ebe4603a8..5a35351e4 100644 --- a/internal/api/client/statuses/statusfave_test.go +++ b/internal/api/client/statuses/statusfave_test.go @@ -89,42 +89,43 @@ func (suite *StatusFaveTestSuite) TestPostFave() { } // try to fave a status that's not faveable -func (suite *StatusFaveTestSuite) TestPostUnfaveable() { - t := suite.testTokens["local_account_1"] - oauthToken := oauth.DBTokenToToken(t) +// TODO: replace this when interaction policies enforced. +// func (suite *StatusFaveTestSuite) TestPostUnfaveable() { +// t := suite.testTokens["local_account_1"] +// oauthToken := oauth.DBTokenToToken(t) - targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable +// targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable - // setup - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) - ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") +// // setup +// recorder := httptest.NewRecorder() +// ctx, _ := testrig.CreateGinTestContext(recorder, nil) +// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) +// ctx.Set(oauth.SessionAuthorizedToken, oauthToken) +// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) +// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) +// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting +// ctx.Request.Header.Set("accept", "application/json") - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: statuses.IDKey, - Value: targetStatus.ID, - }, - } +// // normally the router would populate these params from the path values, +// // but because we're calling the function directly, we need to set them manually. +// ctx.Params = gin.Params{ +// gin.Param{ +// Key: statuses.IDKey, +// Value: targetStatus.ID, +// }, +// } - suite.statusModule.StatusFavePOSTHandler(ctx) +// suite.statusModule.StatusFavePOSTHandler(ctx) - // check response - suite.EqualValues(http.StatusForbidden, recorder.Code) +// // check response +// suite.EqualValues(http.StatusForbidden, recorder.Code) - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b)) -} +// result := recorder.Result() +// defer result.Body.Close() +// b, err := ioutil.ReadAll(result.Body) +// assert.NoError(suite.T(), err) +// assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b)) +// } func TestStatusFaveTestSuite(t *testing.T) { suite.Run(t, new(StatusFaveTestSuite)) diff --git a/internal/api/client/statuses/statuspin_test.go b/internal/api/client/statuses/statuspin_test.go index 39909e6c4..e9b65bd37 100644 --- a/internal/api/client/statuses/statuspin_test.go +++ b/internal/api/client/statuses/statuspin_test.go @@ -183,9 +183,6 @@ func (suite *StatusPinTestSuite) TestPinStatusTooManyPins() { AccountURI: testAccount.URI, Visibility: gtsmodel.VisibilityPublic, Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, status); err != nil { diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 9098cb59d..0d925d211 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -230,12 +230,6 @@ type AdvancedStatusCreateForm struct { type AdvancedVisibilityFlagsForm struct { // This status will be federated beyond the local timeline(s). Federated *bool `form:"federated" json:"federated" xml:"federated"` - // This status can be boosted/reblogged. - Boostable *bool `form:"boostable" json:"boostable" xml:"boostable"` - // This status can be replied to. - Replyable *bool `form:"replyable" json:"replyable" xml:"replyable"` - // This status can be liked/faved. - Likeable *bool `form:"likeable" json:"likeable" xml:"likeable"` } // StatusContentType is the content type with which to parse the submitted status. diff --git a/internal/cache/cache.go b/internal/cache/cache.go index bb910f3e6..5a8a92ca3 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -73,6 +73,7 @@ func (c *Caches) Init() { c.initFollowRequestIDs() c.initInReplyToIDs() c.initInstance() + c.initInteractionApproval() c.initList() c.initListEntry() c.initMarker() @@ -145,6 +146,7 @@ func (c *Caches) Sweep(threshold float64) { c.GTS.FollowRequestIDs.Trim(threshold) c.GTS.InReplyToIDs.Trim(threshold) c.GTS.Instance.Trim(threshold) + c.GTS.InteractionApproval.Trim(threshold) c.GTS.List.Trim(threshold) c.GTS.ListEntry.Trim(threshold) c.GTS.Marker.Trim(threshold) diff --git a/internal/cache/db.go b/internal/cache/db.go index d0fe77649..50acf00d1 100644 --- a/internal/cache/db.go +++ b/internal/cache/db.go @@ -100,6 +100,9 @@ type GTSCaches struct { // Instance provides access to the gtsmodel Instance database cache. Instance StructCache[*gtsmodel.Instance] + // InteractionApproval provides access to the gtsmodel InteractionApproval database cache. + InteractionApproval StructCache[*gtsmodel.InteractionApproval] + // InReplyToIDs provides access to the status in reply to IDs list database cache. InReplyToIDs SliceCache[string] @@ -737,6 +740,39 @@ func (c *Caches) initInstance() { }) } +func (c *Caches) initInteractionApproval() { + // Calculate maximum cache size. + cap := calculateResultCacheMax( + sizeofInteractionApproval(), + config.GetCacheInteractionApprovalMemRatio(), + ) + + log.Infof(nil, "cache size = %d", cap) + + copyF := func(i1 *gtsmodel.InteractionApproval) *gtsmodel.InteractionApproval { + i2 := new(gtsmodel.InteractionApproval) + *i2 = *i1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/interaction.go. + i2.Account = nil + i2.InteractingAccount = nil + + return i2 + } + + c.GTS.InteractionApproval.Init(structr.CacheConfig[*gtsmodel.InteractionApproval]{ + Indices: []structr.IndexConfig{ + {Fields: "ID"}, + {Fields: "URI"}, + }, + MaxSize: cap, + IgnoreErr: ignoreErrors, + Copy: copyF, + }) +} + func (c *Caches) initList() { // Calculate maximum cache size. cap := calculateResultCacheMax( @@ -1188,6 +1224,7 @@ func (c *Caches) initStatusFave() { c.GTS.StatusFave.Init(structr.CacheConfig[*gtsmodel.StatusFave]{ Indices: []structr.IndexConfig{ {Fields: "ID"}, + {Fields: "URI"}, {Fields: "AccountID,StatusID"}, {Fields: "StatusID", Multiple: true}, }, diff --git a/internal/cache/size.go b/internal/cache/size.go index fb1f165c2..4ec30fbb7 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -189,6 +189,7 @@ func totalOfRatios() float64 { config.GetCacheFollowRequestMemRatio() + config.GetCacheFollowRequestIDsMemRatio() + config.GetCacheInstanceMemRatio() + + config.GetCacheInteractionApprovalMemRatio() + config.GetCacheInReplyToIDsMemRatio() + config.GetCacheListMemRatio() + config.GetCacheListEntryMemRatio() + @@ -425,6 +426,19 @@ func sizeofInstance() uintptr { })) } +func sizeofInteractionApproval() uintptr { + return uintptr(size.Of(>smodel.InteractionApproval{ + ID: exampleID, + CreatedAt: exampleTime, + UpdatedAt: exampleTime, + AccountID: exampleID, + InteractingAccountID: exampleID, + InteractionURI: exampleURI, + InteractionType: gtsmodel.InteractionAnnounce, + URI: exampleURI, + })) +} + func sizeofList() uintptr { return uintptr(size.Of(>smodel.List{ ID: exampleID, @@ -591,9 +605,6 @@ func sizeofStatus() uintptr { Language: "en", CreatedWithApplicationID: exampleID, Federated: func() *bool { ok := true; return &ok }(), - Boostable: func() *bool { ok := true; return &ok }(), - Replyable: func() *bool { ok := true; return &ok }(), - Likeable: func() *bool { ok := true; return &ok }(), ActivityStreamsType: ap.ObjectNote, })) } diff --git a/internal/config/config.go b/internal/config/config.go index 8d410f6ac..015213184 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -191,52 +191,53 @@ type HTTPClientConfiguration struct { } type CacheConfiguration struct { - MemoryTarget bytesize.Size `name:"memory-target"` - AccountMemRatio float64 `name:"account-mem-ratio"` - AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` - AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"` - AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"` - ApplicationMemRatio float64 `name:"application-mem-ratio"` - BlockMemRatio float64 `name:"block-mem-ratio"` - BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"` - BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"` - ClientMemRatio float64 `name:"client-mem-ratio"` - EmojiMemRatio float64 `name:"emoji-mem-ratio"` - EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"` - FilterMemRatio float64 `name:"filter-mem-ratio"` - FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"` - FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"` - FollowMemRatio float64 `name:"follow-mem-ratio"` - FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"` - FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"` - FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"` - InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"` - InstanceMemRatio float64 `name:"instance-mem-ratio"` - ListMemRatio float64 `name:"list-mem-ratio"` - ListEntryMemRatio float64 `name:"list-entry-mem-ratio"` - MarkerMemRatio float64 `name:"marker-mem-ratio"` - MediaMemRatio float64 `name:"media-mem-ratio"` - MentionMemRatio float64 `name:"mention-mem-ratio"` - MoveMemRatio float64 `name:"move-mem-ratio"` - NotificationMemRatio float64 `name:"notification-mem-ratio"` - PollMemRatio float64 `name:"poll-mem-ratio"` - PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"` - PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"` - ReportMemRatio float64 `name:"report-mem-ratio"` - StatusMemRatio float64 `name:"status-mem-ratio"` - StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"` - StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"` - StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"` - StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"` - TagMemRatio float64 `name:"tag-mem-ratio"` - ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"` - TokenMemRatio float64 `name:"token-mem-ratio"` - TombstoneMemRatio float64 `name:"tombstone-mem-ratio"` - UserMemRatio float64 `name:"user-mem-ratio"` - UserMuteMemRatio float64 `name:"user-mute-mem-ratio"` - UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"` - WebfingerMemRatio float64 `name:"webfinger-mem-ratio"` - VisibilityMemRatio float64 `name:"visibility-mem-ratio"` + MemoryTarget bytesize.Size `name:"memory-target"` + AccountMemRatio float64 `name:"account-mem-ratio"` + AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` + AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"` + AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"` + ApplicationMemRatio float64 `name:"application-mem-ratio"` + BlockMemRatio float64 `name:"block-mem-ratio"` + BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"` + BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"` + ClientMemRatio float64 `name:"client-mem-ratio"` + EmojiMemRatio float64 `name:"emoji-mem-ratio"` + EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"` + FilterMemRatio float64 `name:"filter-mem-ratio"` + FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"` + FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"` + FollowMemRatio float64 `name:"follow-mem-ratio"` + FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"` + FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"` + FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"` + InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"` + InstanceMemRatio float64 `name:"instance-mem-ratio"` + InteractionApprovalMemRatio float64 `name:"interaction-approval-mem-ratio"` + ListMemRatio float64 `name:"list-mem-ratio"` + ListEntryMemRatio float64 `name:"list-entry-mem-ratio"` + MarkerMemRatio float64 `name:"marker-mem-ratio"` + MediaMemRatio float64 `name:"media-mem-ratio"` + MentionMemRatio float64 `name:"mention-mem-ratio"` + MoveMemRatio float64 `name:"move-mem-ratio"` + NotificationMemRatio float64 `name:"notification-mem-ratio"` + PollMemRatio float64 `name:"poll-mem-ratio"` + PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"` + PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"` + ReportMemRatio float64 `name:"report-mem-ratio"` + StatusMemRatio float64 `name:"status-mem-ratio"` + StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"` + StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"` + StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"` + StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"` + TagMemRatio float64 `name:"tag-mem-ratio"` + ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"` + TokenMemRatio float64 `name:"token-mem-ratio"` + TombstoneMemRatio float64 `name:"tombstone-mem-ratio"` + UserMemRatio float64 `name:"user-mem-ratio"` + UserMuteMemRatio float64 `name:"user-mute-mem-ratio"` + UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"` + WebfingerMemRatio float64 `name:"webfinger-mem-ratio"` + VisibilityMemRatio float64 `name:"visibility-mem-ratio"` } // MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML). diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 8a76cc21a..ba068761e 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -156,51 +156,52 @@ var Defaults = Configuration{ // when TODO items in the size.go source // file have been addressed, these should // be able to make some more sense :D - AccountMemRatio: 5, - AccountNoteMemRatio: 1, - AccountSettingsMemRatio: 0.1, - AccountStatsMemRatio: 2, - ApplicationMemRatio: 0.1, - BlockMemRatio: 2, - BlockIDsMemRatio: 3, - BoostOfIDsMemRatio: 3, - ClientMemRatio: 0.1, - EmojiMemRatio: 3, - EmojiCategoryMemRatio: 0.1, - FilterMemRatio: 0.5, - FilterKeywordMemRatio: 0.5, - FilterStatusMemRatio: 0.5, - FollowMemRatio: 2, - FollowIDsMemRatio: 4, - FollowRequestMemRatio: 2, - FollowRequestIDsMemRatio: 2, - InReplyToIDsMemRatio: 3, - InstanceMemRatio: 1, - ListMemRatio: 1, - ListEntryMemRatio: 2, - MarkerMemRatio: 0.5, - MediaMemRatio: 4, - MentionMemRatio: 2, - MoveMemRatio: 0.1, - NotificationMemRatio: 2, - PollMemRatio: 1, - PollVoteMemRatio: 2, - PollVoteIDsMemRatio: 2, - ReportMemRatio: 1, - StatusMemRatio: 5, - StatusBookmarkMemRatio: 0.5, - StatusBookmarkIDsMemRatio: 2, - StatusFaveMemRatio: 2, - StatusFaveIDsMemRatio: 3, - TagMemRatio: 2, - ThreadMuteMemRatio: 0.2, - TokenMemRatio: 0.75, - TombstoneMemRatio: 0.5, - UserMemRatio: 0.25, - UserMuteMemRatio: 2, - UserMuteIDsMemRatio: 3, - WebfingerMemRatio: 0.1, - VisibilityMemRatio: 2, + AccountMemRatio: 5, + AccountNoteMemRatio: 1, + AccountSettingsMemRatio: 0.1, + AccountStatsMemRatio: 2, + ApplicationMemRatio: 0.1, + BlockMemRatio: 2, + BlockIDsMemRatio: 3, + BoostOfIDsMemRatio: 3, + ClientMemRatio: 0.1, + EmojiMemRatio: 3, + EmojiCategoryMemRatio: 0.1, + FilterMemRatio: 0.5, + FilterKeywordMemRatio: 0.5, + FilterStatusMemRatio: 0.5, + FollowMemRatio: 2, + FollowIDsMemRatio: 4, + FollowRequestMemRatio: 2, + FollowRequestIDsMemRatio: 2, + InReplyToIDsMemRatio: 3, + InstanceMemRatio: 1, + InteractionApprovalMemRatio: 1, + ListMemRatio: 1, + ListEntryMemRatio: 2, + MarkerMemRatio: 0.5, + MediaMemRatio: 4, + MentionMemRatio: 2, + MoveMemRatio: 0.1, + NotificationMemRatio: 2, + PollMemRatio: 1, + PollVoteMemRatio: 2, + PollVoteIDsMemRatio: 2, + ReportMemRatio: 1, + StatusMemRatio: 5, + StatusBookmarkMemRatio: 0.5, + StatusBookmarkIDsMemRatio: 2, + StatusFaveMemRatio: 2, + StatusFaveIDsMemRatio: 3, + TagMemRatio: 2, + ThreadMuteMemRatio: 0.2, + TokenMemRatio: 0.75, + TombstoneMemRatio: 0.5, + UserMemRatio: 0.25, + UserMuteMemRatio: 2, + UserMuteIDsMemRatio: 3, + WebfingerMemRatio: 0.1, + VisibilityMemRatio: 2, }, HTTPClient: HTTPClientConfiguration{ diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index 71a77e753..8dab7ac6a 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -3250,6 +3250,33 @@ func GetCacheInstanceMemRatio() float64 { return global.GetCacheInstanceMemRatio // SetCacheInstanceMemRatio safely sets the value for global configuration 'Cache.InstanceMemRatio' field func SetCacheInstanceMemRatio(v float64) { global.SetCacheInstanceMemRatio(v) } +// GetCacheInteractionApprovalMemRatio safely fetches the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field +func (st *ConfigState) GetCacheInteractionApprovalMemRatio() (v float64) { + st.mutex.RLock() + v = st.config.Cache.InteractionApprovalMemRatio + st.mutex.RUnlock() + return +} + +// SetCacheInteractionApprovalMemRatio safely sets the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field +func (st *ConfigState) SetCacheInteractionApprovalMemRatio(v float64) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.Cache.InteractionApprovalMemRatio = v + st.reloadToViper() +} + +// CacheInteractionApprovalMemRatioFlag returns the flag name for the 'Cache.InteractionApprovalMemRatio' field +func CacheInteractionApprovalMemRatioFlag() string { return "cache-interaction-approval-mem-ratio" } + +// GetCacheInteractionApprovalMemRatio safely fetches the value for global configuration 'Cache.InteractionApprovalMemRatio' field +func GetCacheInteractionApprovalMemRatio() float64 { + return global.GetCacheInteractionApprovalMemRatio() +} + +// SetCacheInteractionApprovalMemRatio safely sets the value for global configuration 'Cache.InteractionApprovalMemRatio' field +func SetCacheInteractionApprovalMemRatio(v float64) { global.SetCacheInteractionApprovalMemRatio(v) } + // GetCacheListMemRatio safely fetches the Configuration value for state's 'Cache.ListMemRatio' field func (st *ConfigState) GetCacheListMemRatio() (v float64) { st.mutex.RLock() diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index 5ed5d91a1..a9554e0d7 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -115,15 +115,6 @@ func (suite *AccountTestSuite) populateTestStatus(testAccountKey string, status if status.Federated == nil { status.Federated = util.Ptr(true) } - if status.Boostable == nil { - status.Boostable = util.Ptr(true) - } - if status.Likeable == nil { - status.Likeable = util.Ptr(true) - } - if status.Replyable == nil { - status.Replyable = util.Ptr(true) - } if inReplyTo != nil { status.InReplyToAccountID = inReplyTo.AccountID diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index e7256c276..57fb661df 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -60,6 +60,7 @@ type DBService struct { db.Emoji db.HeaderFilter db.Instance + db.Interaction db.Filter db.List db.Marker @@ -203,6 +204,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) { db: db, state: state, }, + Interaction: &interactionDB{ + db: db, + state: state, + }, Filter: &filterDB{ db: db, state: state, diff --git a/internal/db/bundb/interaction.go b/internal/db/bundb/interaction.go new file mode 100644 index 000000000..b818f7a60 --- /dev/null +++ b/internal/db/bundb/interaction.go @@ -0,0 +1,149 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package bundb + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/uptrace/bun" +) + +type interactionDB struct { + db *bun.DB + state *state.State +} + +func (r *interactionDB) newInteractionApprovalQ(approval interface{}) *bun.SelectQuery { + return r.db. + NewSelect(). + Model(approval) +} + +func (r *interactionDB) GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error) { + return r.getInteractionApproval( + ctx, + "ID", + func(approval *gtsmodel.InteractionApproval) error { + return r. + newInteractionApprovalQ(approval). + Where("? = ?", bun.Ident("interaction_approval.id"), id). + Scan(ctx) + }, + id, + ) +} + +func (r *interactionDB) GetInteractionApprovalByURI(ctx context.Context, uri string) (*gtsmodel.InteractionApproval, error) { + return r.getInteractionApproval( + ctx, + "URI", + func(approval *gtsmodel.InteractionApproval) error { + return r. + newInteractionApprovalQ(approval). + Where("? = ?", bun.Ident("interaction_approval.uri"), uri). + Scan(ctx) + }, + uri, + ) +} + +func (r *interactionDB) getInteractionApproval( + ctx context.Context, + lookup string, + dbQuery func(*gtsmodel.InteractionApproval) error, + keyParts ...any, +) (*gtsmodel.InteractionApproval, error) { + // Fetch approval from database cache with loader callback + approval, err := r.state.Caches.GTS.InteractionApproval.LoadOne(lookup, func() (*gtsmodel.InteractionApproval, error) { + var approval gtsmodel.InteractionApproval + + // Not cached! Perform database query + if err := dbQuery(&approval); err != nil { + return nil, err + } + + return &approval, nil + }, keyParts...) + if err != nil { + // Error already processed. + return nil, err + } + + if gtscontext.Barebones(ctx) { + // Only a barebones model was requested. + return approval, nil + } + + if err := r.PopulateInteractionApproval(ctx, approval); err != nil { + return nil, err + } + + return approval, nil +} + +func (r *interactionDB) PopulateInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error { + var ( + err error + errs = gtserror.NewMultiError(2) + ) + + if approval.Account == nil { + // Account is not set, fetch from the database. + approval.Account, err = r.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + approval.AccountID, + ) + if err != nil { + errs.Appendf("error populating interactionApproval account: %w", err) + } + } + + if approval.InteractingAccount == nil { + // InteractingAccount is not set, fetch from the database. + approval.InteractingAccount, err = r.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + approval.InteractingAccountID, + ) + if err != nil { + errs.Appendf("error populating interactionApproval interacting account: %w", err) + } + } + + return errs.Combine() +} + +func (r *interactionDB) PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error { + return r.state.Caches.GTS.InteractionApproval.Store(approval, func() error { + _, err := r.db.NewInsert().Model(approval).Exec(ctx) + return err + }) +} + +func (r *interactionDB) DeleteInteractionApprovalByID(ctx context.Context, id string) error { + defer r.state.Caches.GTS.InteractionApproval.Invalidate("ID", id) + + _, err := r.db.NewDelete(). + TableExpr("? AS ?", bun.Ident("interaction_approvals"), bun.Ident("interaction_approval")). + Where("? = ?", bun.Ident("interaction_approval.id"), id). + Exec(ctx) + return err +} diff --git a/internal/db/bundb/migrations/20240620074530_interaction_policy.go b/internal/db/bundb/migrations/20240620074530_interaction_policy.go new file mode 100644 index 000000000..424039a52 --- /dev/null +++ b/internal/db/bundb/migrations/20240620074530_interaction_policy.go @@ -0,0 +1,264 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package migrations + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/log" + + oldmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240620074530_interaction_policy" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + log.Info(ctx, "migrating statuses and account settings to interaction policy model, please wait...") + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + // Add new columns for interaction + // policies + related fields. + type spec struct { + table string + column string + columnType string + defaultVal string + } + for _, spec := range []spec{ + // Statuses. + { + table: "statuses", + column: "interaction_policy", + columnType: "JSONB", + defaultVal: "", + }, + { + table: "statuses", + column: "pending_approval", + columnType: "BOOLEAN", + defaultVal: "DEFAULT false", + }, + { + table: "statuses", + column: "approved_by_uri", + columnType: "varchar", + defaultVal: "", + }, + + // Status faves. + { + table: "status_faves", + column: "pending_approval", + columnType: "BOOLEAN", + defaultVal: "DEFAULT false", + }, + { + table: "status_faves", + column: "approved_by_uri", + columnType: "varchar", + defaultVal: "", + }, + + // Columns that must be added to the + // `account_settings` table to populate + // default interaction policies for + // different status visibilities. + { + table: "account_settings", + column: "interaction_policy_direct", + columnType: "JSONB", + defaultVal: "", + }, + { + table: "account_settings", + column: "interaction_policy_mutuals_only", + columnType: "JSONB", + defaultVal: "", + }, + { + table: "account_settings", + column: "interaction_policy_followers_only", + columnType: "JSONB", + defaultVal: "", + }, + { + table: "account_settings", + column: "interaction_policy_unlocked", + columnType: "JSONB", + defaultVal: "", + }, + { + table: "account_settings", + column: "interaction_policy_public", + columnType: "JSONB", + defaultVal: "", + }, + } { + exists, err := doesColumnExist(ctx, tx, + spec.table, spec.column, + ) + if err != nil { + // Real error. + return err + } else if exists { + // Already created. + continue + } + + args := []any{ + bun.Ident(spec.table), + bun.Ident(spec.column), + bun.Safe(spec.columnType), + } + + qStr := "ALTER TABLE ? ADD COLUMN ? ?" + if spec.defaultVal != "" { + qStr += " ?" + args = append(args, bun.Safe(spec.defaultVal)) + } + + if _, err := tx.ExecContext(ctx, qStr, args...); err != nil { + return err + } + } + + // Select each locally-created status + // with non-default old flags set. + oldStatuses := []oldmodel.Status{} + + if err := tx. + NewSelect(). + Model(&oldStatuses). + Column("id", "likeable", "replyable", "boostable", "visibility"). + Where("? = ?", bun.Ident("local"), true). + WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { + return sq. + Where("? = ?", bun.Ident("likeable"), false). + WhereOr("? = ?", bun.Ident("replyable"), false). + WhereOr("? = ?", bun.Ident("boostable"), false) + }). + Scan(ctx); err != nil { + return err + } + + // For each status found in this way, update + // to new version of interaction policy. + for _, oldStatus := range oldStatuses { + // Start with default policy for this visibility. + v := gtsmodel.Visibility(oldStatus.Visibility) + policy := gtsmodel.DefaultInteractionPolicyFor(v) + + if !*oldStatus.Likeable { + // Only author can like. + policy.CanLike = gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + WithApproval: make(gtsmodel.PolicyValues, 0), + } + } + + if !*oldStatus.Replyable { + // Only author + mentioned can Reply. + policy.CanReply = gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + gtsmodel.PolicyValueMentioned, + }, + WithApproval: make(gtsmodel.PolicyValues, 0), + } + } + + if !*oldStatus.Boostable { + // Only author can Announce. + policy.CanAnnounce = gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + WithApproval: make(gtsmodel.PolicyValues, 0), + } + } + + // Update status with the new interaction policy. + newStatus := >smodel.Status{ + ID: oldStatus.ID, + InteractionPolicy: policy, + } + if _, err := tx. + NewUpdate(). + Model(newStatus). + Column("interaction_policy"). + Where("? = ?", bun.Ident("id"), newStatus.ID). + Exec(ctx); err != nil { + return err + } + } + + // Drop now unused columns from statuses table. + oldColumns := []string{ + "likeable", + "replyable", + "boostable", + } + for _, column := range oldColumns { + if _, err := tx. + NewDropColumn(). + Table("statuses"). + Column(column). + Exec(ctx); err != nil { + return err + } + } + + // Add new indexes. + if _, err := tx. + NewCreateIndex(). + Table("statuses"). + Index("statuses_pending_approval_idx"). + Column("pending_approval"). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + if _, err := tx. + NewCreateIndex(). + Table("status_faves"). + Index("status_faves_pending_approval_idx"). + Column("pending_approval"). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + return nil + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/migrations/20240620074530_interaction_policy/status.go b/internal/db/bundb/migrations/20240620074530_interaction_policy/status.go new file mode 100644 index 000000000..ae96d047d --- /dev/null +++ b/internal/db/bundb/migrations/20240620074530_interaction_policy/status.go @@ -0,0 +1,61 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import ( + "time" +) + +type Status struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + FetchedAt time.Time `bun:"type:timestamptz,nullzero"` + PinnedAt time.Time `bun:"type:timestamptz,nullzero"` + URI string `bun:",unique,nullzero,notnull"` + URL string `bun:",nullzero"` + Content string `bun:""` + AttachmentIDs []string `bun:"attachments,array"` + TagIDs []string `bun:"tags,array"` + MentionIDs []string `bun:"mentions,array"` + EmojiIDs []string `bun:"emojis,array"` + Local *bool `bun:",nullzero,notnull,default:false"` + AccountID string `bun:"type:CHAR(26),nullzero,notnull"` + AccountURI string `bun:",nullzero,notnull"` + InReplyToID string `bun:"type:CHAR(26),nullzero"` + InReplyToURI string `bun:",nullzero"` + InReplyToAccountID string `bun:"type:CHAR(26),nullzero"` + InReplyTo *Status `bun:"-"` + BoostOfID string `bun:"type:CHAR(26),nullzero"` + BoostOfURI string `bun:"-"` + BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` + BoostOf *Status `bun:"-"` + ThreadID string `bun:"type:CHAR(26),nullzero"` + PollID string `bun:"type:CHAR(26),nullzero"` + ContentWarning string `bun:",nullzero"` + Visibility string `bun:",nullzero,notnull"` + Sensitive *bool `bun:",nullzero,notnull,default:false"` + Language string `bun:",nullzero"` + CreatedWithApplicationID string `bun:"type:CHAR(26),nullzero"` + ActivityStreamsType string `bun:",nullzero,notnull"` + Text string `bun:""` + Federated *bool `bun:",notnull"` + Boostable *bool `bun:",notnull"` + Replyable *bool `bun:",notnull"` + Likeable *bool `bun:",notnull"` +} diff --git a/internal/db/bundb/status_test.go b/internal/db/bundb/status_test.go index 4f0db5e2c..0111dc6e7 100644 --- a/internal/db/bundb/status_test.go +++ b/internal/db/bundb/status_test.go @@ -44,9 +44,6 @@ func (suite *StatusTestSuite) TestGetStatusByID() { suite.Nil(status.InReplyTo) suite.Nil(status.InReplyToAccount) suite.True(*status.Federated) - suite.True(*status.Boostable) - suite.True(*status.Replyable) - suite.True(*status.Likeable) } func (suite *StatusTestSuite) TestGetStatusesByIDs() { @@ -73,9 +70,6 @@ func (suite *StatusTestSuite) TestGetStatusesByIDs() { suite.Nil(status1.InReplyTo) suite.Nil(status1.InReplyToAccount) suite.True(*status1.Federated) - suite.True(*status1.Boostable) - suite.True(*status1.Replyable) - suite.True(*status1.Likeable) status2 := statuses[1] suite.NotNil(status2) @@ -86,9 +80,6 @@ func (suite *StatusTestSuite) TestGetStatusesByIDs() { suite.Nil(status2.InReplyTo) suite.Nil(status2.InReplyToAccount) suite.True(*status2.Federated) - suite.True(*status2.Boostable) - suite.False(*status2.Replyable) - suite.False(*status2.Likeable) } func (suite *StatusTestSuite) TestGetStatusByURI() { @@ -104,9 +95,6 @@ func (suite *StatusTestSuite) TestGetStatusByURI() { suite.Nil(status.InReplyTo) suite.Nil(status.InReplyToAccount) suite.True(*status.Federated) - suite.True(*status.Boostable) - suite.False(*status.Replyable) - suite.False(*status.Likeable) } func (suite *StatusTestSuite) TestGetStatusWithExtras() { @@ -121,9 +109,6 @@ func (suite *StatusTestSuite) TestGetStatusWithExtras() { suite.NotEmpty(status.Attachments) suite.NotEmpty(status.Emojis) suite.True(*status.Federated) - suite.True(*status.Boostable) - suite.True(*status.Replyable) - suite.True(*status.Likeable) } func (suite *StatusTestSuite) TestGetStatusWithMention() { @@ -138,9 +123,6 @@ func (suite *StatusTestSuite) TestGetStatusWithMention() { suite.NotEmpty(status.InReplyToID) suite.NotEmpty(status.InReplyToAccountID) suite.True(*status.Federated) - suite.True(*status.Boostable) - suite.True(*status.Replyable) - suite.True(*status.Likeable) } // The below test was originally used to ensure that a second diff --git a/internal/db/bundb/statusfave.go b/internal/db/bundb/statusfave.go index 8e9ff501c..e3daa876b 100644 --- a/internal/db/bundb/statusfave.go +++ b/internal/db/bundb/statusfave.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "slices" + "time" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" @@ -77,6 +78,21 @@ func (s *statusFaveDB) GetStatusFaveByID(ctx context.Context, id string) (*gtsmo ) } +func (s *statusFaveDB) GetStatusFaveByURI(ctx context.Context, uri string) (*gtsmodel.StatusFave, error) { + return s.getStatusFave( + ctx, + "URI", + func(fave *gtsmodel.StatusFave) error { + return s.db. + NewSelect(). + Model(fave). + Where("? = ?", bun.Ident("uri"), uri). + Scan(ctx) + }, + uri, + ) +} + func (s *statusFaveDB) getStatusFave(ctx context.Context, lookup string, dbQuery func(*gtsmodel.StatusFave) error, keyParts ...any) (*gtsmodel.StatusFave, error) { // Fetch status fave from database cache with loader callback fave, err := s.state.Caches.GTS.StatusFave.LoadOne(lookup, func() (*gtsmodel.StatusFave, error) { @@ -242,6 +258,26 @@ func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusF }) } +func (s *statusFaveDB) UpdateStatusFave(ctx context.Context, fave *gtsmodel.StatusFave, columns ...string) error { + fave.UpdatedAt = time.Now() + if len(columns) > 0 { + // If we're updating by column, + // ensure "updated_at" is included. + columns = append(columns, "updated_at") + } + + // Update the status fave model in the database. + return s.state.Caches.GTS.StatusFave.Store(fave, func() error { + _, err := s.db. + NewUpdate(). + Model(fave). + Where("? = ?", bun.Ident("status_fave.id"), fave.ID). + Column(columns...). + Exec(ctx) + return err + }) +} + func (s *statusFaveDB) DeleteStatusFaveByID(ctx context.Context, id string) error { var statusID string diff --git a/internal/db/bundb/timeline_test.go b/internal/db/bundb/timeline_test.go index 98ae6b20f..0a014d321 100644 --- a/internal/db/bundb/timeline_test.go +++ b/internal/db/bundb/timeline_test.go @@ -65,9 +65,7 @@ func getFutureStatus() *gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), + InteractionPolicy: gtsmodel.DefaultInteractionPolicyPublic(), ActivityStreamsType: ap.ObjectNote, } } diff --git a/internal/db/db.go b/internal/db/db.go index 330766306..a148d778a 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -32,6 +32,7 @@ type DB interface { Emoji HeaderFilter Instance + Interaction Filter List Marker diff --git a/internal/db/interaction.go b/internal/db/interaction.go new file mode 100644 index 000000000..6f595c54e --- /dev/null +++ b/internal/db/interaction.go @@ -0,0 +1,41 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package db + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type Interaction interface { + // GetInteractionApprovalByID gets one approval with the given id. + GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error) + + // GetInteractionApprovalByID gets one approval with the given uri. + GetInteractionApprovalByURI(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error) + + // PopulateInteractionApproval ensures that the approval's struct fields are populated. + PopulateInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error + + // PutInteractionApproval puts a new approval in the database. + PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error + + // DeleteInteractionApprovalByID deletes one approval with the given ID. + DeleteInteractionApprovalByID(ctx context.Context, id string) error +} diff --git a/internal/db/statusfave.go b/internal/db/statusfave.go index 343a80caa..192ef436b 100644 --- a/internal/db/statusfave.go +++ b/internal/db/statusfave.go @@ -27,9 +27,12 @@ type StatusFave interface { // GetStatusFaveByAccountID gets one status fave created by the given accountID, targeting the given statusID. GetStatusFave(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, error) - // GetStatusFave returns one status fave with the given id. + // GetStatusFaveByID returns one status fave with the given id. GetStatusFaveByID(ctx context.Context, id string) (*gtsmodel.StatusFave, error) + // GetStatusFaveByURI returns one status fave with the given uri. + GetStatusFaveByURI(ctx context.Context, uri string) (*gtsmodel.StatusFave, error) + // GetStatusFaves returns a slice of faves/likes of the status with given ID. // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, error) @@ -40,6 +43,9 @@ type StatusFave interface { // PutStatusFave inserts the given statusFave into the database. PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error + // UpdateStatusFave updates one statusFave in the database. + UpdateStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave, columns ...string) error + // DeleteStatusFave deletes one status fave with the given id. DeleteStatusFaveByID(ctx context.Context, id string) error diff --git a/internal/federation/dereferencing/announce.go b/internal/federation/dereferencing/announce.go index 6516bdced..51f1ffcdd 100644 --- a/internal/federation/dereferencing/announce.go +++ b/internal/federation/dereferencing/announce.go @@ -92,9 +92,6 @@ func (d *Dereferencer) EnrichAnnounce( boost.BoostOfAccount = target.Account boost.Visibility = target.Visibility boost.Federated = target.Federated - boost.Boostable = target.Boostable - boost.Replyable = target.Replyable - boost.Likeable = target.Likeable // Store the boost wrapper status in database. switch err = d.state.DB.PutStatus(ctx, boost); { diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index 406534457..0e227a0c1 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -633,9 +633,7 @@ func (d *Dereferencer) isPermittedStatus( } } - if permitted && - *status.InReplyTo.Replyable { - // Status is reply-able to. + if permitted { return true, nil } diff --git a/internal/federation/dereferencing/status_test.go b/internal/federation/dereferencing/status_test.go index 2d0085cce..3b2c2bff2 100644 --- a/internal/federation/dereferencing/status_test.go +++ b/internal/federation/dereferencing/status_test.go @@ -56,9 +56,6 @@ func (suite *StatusTestSuite) TestDereferenceSimpleStatus() { suite.NoError(err) suite.Equal(status.ID, dbStatus.ID) suite.True(*dbStatus.Federated) - suite.True(*dbStatus.Boostable) - suite.True(*dbStatus.Replyable) - suite.True(*dbStatus.Likeable) // account should be in the database now too account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) @@ -96,9 +93,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithMention() { suite.NoError(err) suite.Equal(status.ID, dbStatus.ID) suite.True(*dbStatus.Federated) - suite.True(*dbStatus.Boostable) - suite.True(*dbStatus.Replyable) - suite.True(*dbStatus.Likeable) // account should be in the database now too account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) @@ -151,9 +145,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithTag() { suite.NoError(err) suite.Equal(status.ID, dbStatus.ID) suite.True(*dbStatus.Federated) - suite.True(*dbStatus.Boostable) - suite.True(*dbStatus.Replyable) - suite.True(*dbStatus.Likeable) // account should be in the database now too account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) @@ -197,9 +188,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithImageAndNoContent() { suite.NoError(err) suite.Equal(status.ID, dbStatus.ID) suite.True(*dbStatus.Federated) - suite.True(*dbStatus.Boostable) - suite.True(*dbStatus.Replyable) - suite.True(*dbStatus.Likeable) // account should be in the database now too account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) diff --git a/internal/filter/visibility/boostable.go b/internal/filter/visibility/boostable.go index 7c8bda324..7362ad45c 100644 --- a/internal/filter/visibility/boostable.go +++ b/internal/filter/visibility/boostable.go @@ -53,10 +53,5 @@ func (f *Filter) StatusBoostable(ctx context.Context, requester *gtsmodel.Accoun return false, nil } - if !*status.Boostable { - log.Trace(ctx, "status marked not boostable") - return false, nil - } - return true, nil } diff --git a/internal/filter/visibility/home_timeline_test.go b/internal/filter/visibility/home_timeline_test.go index d8211c8dd..9b7ce8c51 100644 --- a/internal/filter/visibility/home_timeline_test.go +++ b/internal/filter/visibility/home_timeline_test.go @@ -161,9 +161,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestThread() { Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { @@ -214,9 +211,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly( Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, originalStatus); err != nil { @@ -248,9 +242,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly( Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { @@ -282,9 +273,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly( Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil { @@ -327,9 +315,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, originalStatus); err != nil { @@ -361,9 +346,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { @@ -395,9 +377,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, } if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil { diff --git a/internal/gtsmodel/accountsettings.go b/internal/gtsmodel/accountsettings.go index 109d90ad9..592a2330d 100644 --- a/internal/gtsmodel/accountsettings.go +++ b/internal/gtsmodel/accountsettings.go @@ -21,15 +21,20 @@ import "time" // AccountSettings models settings / preferences for a local, non-instance account. type AccountSettings struct { - AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings. - CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created. - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated. - Privacy Visibility `bun:",nullzero"` // Default post privacy for this account - Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default? - Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in? - StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts). - Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set). - CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses. - EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed - HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections. + AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created. + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated. + Privacy Visibility `bun:",nullzero"` // Default post privacy for this account + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default? + Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in? + StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts). + Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set). + CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses. + EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed + HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections. + InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy. + InteractionPolicyMutualsOnly *InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy. + InteractionPolicyFollowersOnly *InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy. + InteractionPolicyUnlocked *InteractionPolicy `bun:""` // Interaction policy to use for new unlocked visibility statuses. If null, assume default policy. + InteractionPolicyPublic *InteractionPolicy `bun:""` // Interaction policy to use for new public visibility statuses. If null, assume default policy. } diff --git a/internal/gtsmodel/interactionapproval.go b/internal/gtsmodel/interactionapproval.go new file mode 100644 index 000000000..f6a5da83b --- /dev/null +++ b/internal/gtsmodel/interactionapproval.go @@ -0,0 +1,55 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import "time" + +// InteractionApproval refers to a single Accept activity sent +// *from this instance* in response to an interaction request, +// in order to approve it. +// +// Accepts originating from remote instances are not stored +// using this format; the URI of the remote Accept is instead +// just added to the *gtsmodel.StatusFave or *gtsmodel.Status. +type InteractionApproval struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated + AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the account that owns this accept/approval + Account *Account `bun:"-"` // account corresponding to accountID + InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the account that did the interaction that this Accept targets. + InteractingAccount *Account `bun:"-"` // account corresponding to targetAccountID + InteractionURI string `bun:",nullzero,notnull"` // URI of the target like, reply, or announce + InteractionType InteractionType `bun:",notnull"` // One of Like, Reply, or Announce. + URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI of the Accept. +} + +// Like / Reply / Announce +type InteractionType int + +const ( + // WARNING: DO NOT CHANGE THE ORDER OF THESE, + // as this will cause breakage of approvals! + // + // If you need to add new interaction types, + // add them *to the end* of the list. + + InteractionLike InteractionType = iota + InteractionReply + InteractionAnnounce +) diff --git a/internal/gtsmodel/interactionpolicy.go b/internal/gtsmodel/interactionpolicy.go new file mode 100644 index 000000000..ecb525b47 --- /dev/null +++ b/internal/gtsmodel/interactionpolicy.go @@ -0,0 +1,314 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +// A policy URI is GoToSocial's internal representation of +// one ActivityPub URI for an Actor or a Collection of Actors, +// specific to the domain of enforcing interaction policies. +// +// A PolicyValue can be stored in the database either as one +// of the Value constants defined below (to save space), OR as +// a full-fledged ActivityPub URI. +// +// A PolicyValue should be translated to the canonical string +// value of the represented URI when federating an item, or +// from the canonical string value of the URI when receiving +// or retrieving an item. +// +// For example, if the PolicyValue `followers` was being +// federated outwards in an interaction policy attached to an +// item created by the actor `https://example.org/users/someone`, +// then it should be translated to their followers URI when sent, +// eg., `https://example.org/users/someone/followers`. +// +// Likewise, if GoToSocial receives an item with an interaction +// policy containing `https://example.org/users/someone/followers`, +// and the item was created by `https://example.org/users/someone`, +// then the followers URI would be converted to `followers` +// for internal storage. +type PolicyValue string + +const ( + // Stand-in for ActivityPub magic public URI, + // which encompasses every possible Actor URI. + PolicyValuePublic PolicyValue = "public" + // Stand-in for the Followers Collection of + // the item owner's Actor. + PolicyValueFollowers PolicyValue = "followers" + // Stand-in for the Following Collection of + // the item owner's Actor. + PolicyValueFollowing PolicyValue = "following" + // Stand-in for the Mutuals Collection of + // the item owner's Actor. + // + // (TODO: Reserved, currently unused). + PolicyValueMutuals PolicyValue = "mutuals" + // Stand-in for Actor URIs tagged in the item. + PolicyValueMentioned PolicyValue = "mentioned" + // Stand-in for the Actor URI of the item owner. + PolicyValueAuthor PolicyValue = "author" +) + +// FeasibleForVisibility returns true if the PolicyValue could feasibly +// be set in a policy for an item with the given visibility, otherwise +// returns false. +// +// For example, PolicyValuePublic could not be set in a policy for an +// item with visibility FollowersOnly, but could be set in a policy +// for an item with visibility Public or Unlocked. +// +// This is not prescriptive, and should be used only to guide policy +// choices. Eg., if a remote instance wants to do something wacky like +// set "anyone can interact with this status" for a Direct visibility +// status, that's their business; our normal visibility filtering will +// prevent users on our instance from actually being able to interact +// unless they can see the status anyway. +func (p PolicyValue) FeasibleForVisibility(v Visibility) bool { + switch p { + + // Mentioned and self Values are + // feasible for any visibility. + case PolicyValueAuthor, + PolicyValueMentioned: + return true + + // Followers/following/mutual Values + // are only feasible for items with + // followers visibility and higher. + case PolicyValueFollowers, + PolicyValueFollowing: + return v == VisibilityFollowersOnly || + v == VisibilityPublic || + v == VisibilityUnlocked + + // Public policy Value only feasible + // for items that are To or CC public. + case PolicyValuePublic: + return v == VisibilityUnlocked || + v == VisibilityPublic + + // Any other combo + // is probably fine. + default: + return true + } +} + +type PolicyValues []PolicyValue + +// PolicyResult represents the result of +// checking an Actor URI and interaction +// type against the conditions of an +// InteractionPolicy to determine if that +// interaction is permitted. +type PolicyResult int + +const ( + // Interaction is forbidden for this + // PolicyValue + interaction combination. + PolicyResultForbidden PolicyResult = iota + // Interaction is conditionally permitted + // for this PolicyValue + interaction combo, + // pending approval by the item owner. + PolicyResultWithApproval + // Interaction is permitted for this + // PolicyValue + interaction combination. + PolicyResultPermitted +) + +// An InteractionPolicy determines which +// interactions will be accepted for an +// item, and according to what rules. +type InteractionPolicy struct { + // Conditions in which a Like + // interaction will be accepted + // for an item with this policy. + CanLike PolicyRules + // Conditions in which a Reply + // interaction will be accepted + // for an item with this policy. + CanReply PolicyRules + // Conditions in which an Announce + // interaction will be accepted + // for an item with this policy. + CanAnnounce PolicyRules +} + +// PolicyRules represents the rules according +// to which a certain interaction is permitted +// to various Actor and Actor Collection URIs. +type PolicyRules struct { + // Always is for PolicyValues who are + // permitted to do an interaction + // without requiring approval. + Always PolicyValues + // WithApproval is for PolicyValues who + // are conditionally permitted to do + // an interaction, pending approval. + WithApproval PolicyValues +} + +// Returns the default interaction policy +// for the given visibility level. +func DefaultInteractionPolicyFor(v Visibility) *InteractionPolicy { + switch v { + case VisibilityPublic: + return DefaultInteractionPolicyPublic() + case VisibilityUnlocked: + return DefaultInteractionPolicyUnlocked() + case VisibilityFollowersOnly, VisibilityMutualsOnly: + return DefaultInteractionPolicyFollowersOnly() + case VisibilityDirect: + return DefaultInteractionPolicyDirect() + default: + panic("visibility " + v + " not recognized") + } +} + +// Returns the default interaction policy +// for a post with visibility of public. +func DefaultInteractionPolicyPublic() *InteractionPolicy { + // Anyone can like. + canLikeAlways := make(PolicyValues, 1) + canLikeAlways[0] = PolicyValuePublic + + // Unused, set empty. + canLikeWithApproval := make(PolicyValues, 0) + + // Anyone can reply. + canReplyAlways := make(PolicyValues, 1) + canReplyAlways[0] = PolicyValuePublic + + // Unused, set empty. + canReplyWithApproval := make(PolicyValues, 0) + + // Anyone can announce. + canAnnounceAlways := make(PolicyValues, 1) + canAnnounceAlways[0] = PolicyValuePublic + + // Unused, set empty. + canAnnounceWithApproval := make(PolicyValues, 0) + + return &InteractionPolicy{ + CanLike: PolicyRules{ + Always: canLikeAlways, + WithApproval: canLikeWithApproval, + }, + CanReply: PolicyRules{ + Always: canReplyAlways, + WithApproval: canReplyWithApproval, + }, + CanAnnounce: PolicyRules{ + Always: canAnnounceAlways, + WithApproval: canAnnounceWithApproval, + }, + } +} + +// Returns the default interaction policy +// for a post with visibility of unlocked. +func DefaultInteractionPolicyUnlocked() *InteractionPolicy { + // Same as public (for now). + return DefaultInteractionPolicyPublic() +} + +// Returns the default interaction policy for +// a post with visibility of followers only. +func DefaultInteractionPolicyFollowersOnly() *InteractionPolicy { + // Self, followers and mentioned can like. + canLikeAlways := make(PolicyValues, 3) + canLikeAlways[0] = PolicyValueAuthor + canLikeAlways[1] = PolicyValueFollowers + canLikeAlways[2] = PolicyValueMentioned + + // Unused, set empty. + canLikeWithApproval := make(PolicyValues, 0) + + // Self, followers and mentioned can reply. + canReplyAlways := make(PolicyValues, 3) + canReplyAlways[0] = PolicyValueAuthor + canReplyAlways[1] = PolicyValueFollowers + canReplyAlways[2] = PolicyValueMentioned + + // Unused, set empty. + canReplyWithApproval := make(PolicyValues, 0) + + // Only self can announce. + canAnnounceAlways := make(PolicyValues, 1) + canAnnounceAlways[0] = PolicyValueAuthor + + // Unused, set empty. + canAnnounceWithApproval := make(PolicyValues, 0) + + return &InteractionPolicy{ + CanLike: PolicyRules{ + Always: canLikeAlways, + WithApproval: canLikeWithApproval, + }, + CanReply: PolicyRules{ + Always: canReplyAlways, + WithApproval: canReplyWithApproval, + }, + CanAnnounce: PolicyRules{ + Always: canAnnounceAlways, + WithApproval: canAnnounceWithApproval, + }, + } +} + +// Returns the default interaction policy +// for a post with visibility of direct. +func DefaultInteractionPolicyDirect() *InteractionPolicy { + // Mentioned and self can always like. + canLikeAlways := make(PolicyValues, 2) + canLikeAlways[0] = PolicyValueAuthor + canLikeAlways[1] = PolicyValueMentioned + + // Unused, set empty. + canLikeWithApproval := make(PolicyValues, 0) + + // Mentioned and self can always reply. + canReplyAlways := make(PolicyValues, 2) + canReplyAlways[0] = PolicyValueAuthor + canReplyAlways[1] = PolicyValueMentioned + + // Unused, set empty. + canReplyWithApproval := make(PolicyValues, 0) + + // Only self can announce. + canAnnounceAlways := make(PolicyValues, 1) + canAnnounceAlways[0] = PolicyValueAuthor + + // Unused, set empty. + canAnnounceWithApproval := make(PolicyValues, 0) + + return &InteractionPolicy{ + CanLike: PolicyRules{ + Always: canLikeAlways, + WithApproval: canLikeWithApproval, + }, + CanReply: PolicyRules{ + Always: canReplyAlways, + WithApproval: canReplyWithApproval, + }, + CanAnnounce: PolicyRules{ + Always: canAnnounceAlways, + WithApproval: canAnnounceWithApproval, + }, + } +} diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index 0f946ed0f..5cf6b061a 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -39,12 +39,15 @@ type NotificationType string // Notification Types const ( - NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you - NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you - NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status - NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses - NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses - NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended - NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status. - NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance. + NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you + NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you + NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status + NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses + NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses + NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended + NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status. + NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance. + NotificationPendingFave NotificationType = "pending.favourite" // Someone has faved a status of yours, which requires approval by you. + NotificationPendingReply NotificationType = "pending.reply" // Someone has replied to a status of yours, which requires approval by you. + NotificationPendingReblog NotificationType = "pending.reblog" // Someone has boosted a status of yours, which requires approval by you. ) diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 3bbe82c08..221663ccd 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -66,9 +66,9 @@ type Status struct { ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!. Text string `bun:""` // Original text of the status without formatting Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s) - Boostable *bool `bun:",notnull"` // This status can be boosted/reblogged - Replyable *bool `bun:",notnull"` // This status can be replied to - Likeable *bool `bun:",notnull"` // This status can be liked/faved + InteractionPolicy *InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers. + PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed. + ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to. } // GetID implements timeline.Timelineable{}. diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index f81226f8b..644b3ca63 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -31,4 +31,6 @@ type StatusFave struct { StatusID string `bun:"type:CHAR(26),unique:statusfaveaccountstatus,nullzero,notnull"` // database id of the status that has been 'faved' Status *Status `bun:"-"` // the faved status URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI of this fave + PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then Like must be Approved by the like-ee before being fully distributed. + ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves this Like. } diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index 80cc65c7f..8898181ae 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -185,11 +185,6 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac return errWithCode } - if !*inReplyTo.Replyable { - const text = "in-reply-to status marked as not replyable" - return gtserror.NewErrorForbidden(errors.New(text), text) - } - // Set status fields from inReplyTo. status.InReplyToID = inReplyTo.ID status.InReplyTo = inReplyTo @@ -289,9 +284,6 @@ func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.Advanced func processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error { // by default all flags are set to true federated := true - boostable := true - replyable := true - likeable := true // If visibility isn't set on the form, then just take the account default. // If that's also not set, take the default for the whole instance. @@ -305,57 +297,10 @@ func processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVi vis = gtsmodel.VisibilityDefault } - switch vis { - case gtsmodel.VisibilityPublic: - // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out - break - case gtsmodel.VisibilityUnlocked: - // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them - if form.Federated != nil { - federated = *form.Federated - } - - if form.Boostable != nil { - boostable = *form.Boostable - } - - if form.Replyable != nil { - replyable = *form.Replyable - } - - if form.Likeable != nil { - likeable = *form.Likeable - } - - case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: - // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them - boostable = false - - if form.Federated != nil { - federated = *form.Federated - } - - if form.Replyable != nil { - replyable = *form.Replyable - } - - if form.Likeable != nil { - likeable = *form.Likeable - } - - case gtsmodel.VisibilityDirect: - // direct is pretty easy: there's only one possible setting so return it - federated = true - boostable = false - replyable = true - likeable = true - } + // Todo: sort out likeable/replyable/boostable in next PR. status.Visibility = vis status.Federated = &federated - status.Boostable = &boostable - status.Replyable = &replyable - status.Likeable = &likeable return nil } diff --git a/internal/processing/status/create_test.go b/internal/processing/status/create_test.go index e32d8bc52..a8211d1c1 100644 --- a/internal/processing/status/create_test.go +++ b/internal/processing/status/create_test.go @@ -53,9 +53,6 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks( }, AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, }, } @@ -87,9 +84,6 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuot }, AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, }, } @@ -125,9 +119,6 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji }, AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, }, } @@ -159,9 +150,6 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj }, AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, }, } @@ -197,9 +185,6 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() { }, AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, }, } @@ -229,9 +214,6 @@ func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() { }, AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, }, } @@ -266,9 +248,6 @@ func (suite *StatusCreateTestSuite) TestProcessReplyToUnthreadedRemoteStatus() { }, AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, }, } diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go index dd961c082..49dacf18d 100644 --- a/internal/processing/status/fave.go +++ b/internal/processing/status/fave.go @@ -62,11 +62,6 @@ func (p *Processor) getFaveableStatus( return nil, nil, errWithCode } - if !*target.Likeable { - err := errors.New("status is not faveable") - return nil, nil, gtserror.NewErrorForbidden(err, err.Error()) - } - fave, err := p.state.DB.GetStatusFave(ctx, requester.ID, target.ID) if err != nil && !errors.Is(err, db.ErrNoEntries) { err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err) diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go index 15be23baf..49a68d27a 100644 --- a/internal/processing/workers/fromclientapi_test.go +++ b/internal/processing/workers/fromclientapi_test.go @@ -69,9 +69,6 @@ func (suite *FromClientAPITestSuite) newStatus( Visibility: visibility, ActivityStreamsType: ap.ObjectNote, Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), } if replyToStatus != nil { diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go index b28927f39..705795af4 100644 --- a/internal/processing/workers/fromfediapi_test.go +++ b/internal/processing/workers/fromfediapi_test.go @@ -89,77 +89,78 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() { suite.False(*notif.Read) } -func (suite *FromFediAPITestSuite) TestProcessReplyMention() { - testStructs := suite.SetupTestStructs() - defer suite.TearDownTestStructs(testStructs) +// Todo: fix this test up in interaction policies PR. +// func (suite *FromFediAPITestSuite) TestProcessReplyMention() { +// testStructs := suite.SetupTestStructs() +// defer suite.TearDownTestStructs(testStructs) - repliedAccount := suite.testAccounts["local_account_1"] - repliedStatus := suite.testStatuses["local_account_1_status_1"] - replyingAccount := suite.testAccounts["remote_account_1"] +// repliedAccount := suite.testAccounts["local_account_1"] +// repliedStatus := suite.testStatuses["local_account_1_status_1"] +// replyingAccount := suite.testAccounts["remote_account_1"] - // Set the replyingAccount's last fetched_at - // date to something recent so no refresh is attempted, - // and ensure it isn't a suspended account. - replyingAccount.FetchedAt = time.Now() - replyingAccount.SuspendedAt = time.Time{} - replyingAccount.SuspensionOrigin = "" - err := testStructs.State.DB.UpdateAccount(context.Background(), - replyingAccount, - "fetched_at", - "suspended_at", - "suspension_origin", - ) - suite.NoError(err) +// // Set the replyingAccount's last fetched_at +// // date to something recent so no refresh is attempted, +// // and ensure it isn't a suspended account. +// replyingAccount.FetchedAt = time.Now() +// replyingAccount.SuspendedAt = time.Time{} +// replyingAccount.SuspensionOrigin = "" +// err := testStructs.State.DB.UpdateAccount(context.Background(), +// replyingAccount, +// "fetched_at", +// "suspended_at", +// "suspension_origin", +// ) +// suite.NoError(err) - // Get replying statusable to use from remote test statuses. - const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552" - replyingStatusable := testrig.NewTestFediStatuses()[replyingURI] - ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI)) +// // Get replying statusable to use from remote test statuses. +// const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552" +// replyingStatusable := testrig.NewTestFediStatuses()[replyingURI] +// ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI)) - // Open a websocket stream to later test the streamed status reply. - wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) - suite.NoError(errWithCode) +// // Open a websocket stream to later test the streamed status reply. +// wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) +// suite.NoError(errWithCode) - // Send the replied status off to the fedi worker to be further processed. - err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{ - APObjectType: ap.ObjectNote, - APActivityType: ap.ActivityCreate, - APObject: replyingStatusable, - Receiving: repliedAccount, - Requesting: replyingAccount, - }) - suite.NoError(err) +// // Send the replied status off to the fedi worker to be further processed. +// err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{ +// APObjectType: ap.ObjectNote, +// APActivityType: ap.ActivityCreate, +// APObject: replyingStatusable, +// Receiving: repliedAccount, +// Requesting: replyingAccount, +// }) +// suite.NoError(err) - // side effects should be triggered - // 1. status should be in the database - replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI) - suite.NoError(err) +// // side effects should be triggered +// // 1. status should be in the database +// replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI) +// suite.NoError(err) - // 2. a notification should exist for the mention - var notif gtsmodel.Notification - err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{ - {Key: "status_id", Value: replyingStatus.ID}, - }, ¬if) - suite.NoError(err) - suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) - suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) - suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) - suite.Equal(replyingStatus.ID, notif.StatusID) - suite.False(*notif.Read) +// // 2. a notification should exist for the mention +// var notif gtsmodel.Notification +// err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{ +// {Key: "status_id", Value: replyingStatus.ID}, +// }, ¬if) +// suite.NoError(err) +// suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) +// suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) +// suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) +// suite.Equal(replyingStatus.ID, notif.StatusID) +// suite.False(*notif.Read) - ctx, _ := context.WithTimeout(context.Background(), time.Second*5) - msg, ok := wssStream.Recv(ctx) - suite.True(ok) +// 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) -} +// suite.Equal(stream.EventTypeNotification, msg.Event) +// suite.NotEmpty(msg.Payload) +// suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) +// notifStreamed := &apimodel.Notification{} +// err = json.Unmarshal([]byte(msg.Payload), notifStreamed) +// suite.NoError(err) +// suite.Equal("mention", notifStreamed.Type) +// suite.Equal(replyingAccount.ID, notifStreamed.Account.ID) +// } func (suite *FromFediAPITestSuite) TestProcessFave() { testStructs := suite.SetupTestStructs() diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index ba370790a..cb3e320d9 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -399,9 +399,6 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // needs to be created for this in go-fed/activity. // Until this is implemented, assume all true. status.Federated = util.Ptr(true) - status.Boostable = util.Ptr(true) - status.Replyable = util.Ptr(true) - status.Likeable = util.Ptr(true) // status.Sensitive sensitive := ap.ExtractSensitive(statusable) diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index 2ee2f9607..6c49e332b 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -181,9 +181,6 @@ func (suite *ASToInternalTestSuite) TestParseReplyWithMention() { suite.Equal(inReplyToStatus.ID, status.InReplyToID) suite.Equal(inReplyToStatus.URI, status.InReplyToURI) suite.True(*status.Federated) - suite.True(*status.Boostable) - suite.True(*status.Replyable) - suite.True(*status.Likeable) suite.Equal(`

@the_mighty_zork nice there it is:

social.pixie.town/users/f0x/st

`, status.Content) suite.Len(status.Mentions, 1) m1 := status.Mentions[0] diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index 095e2b121..76ec6232c 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -95,9 +95,6 @@ func (c *Converter) StatusToBoost( BoostOfAccount: target.Account, Visibility: target.Visibility, Federated: util.Ptr(*target.Federated), - Boostable: util.Ptr(*target.Boostable), - Replyable: util.Ptr(*target.Replyable), - Likeable: util.Ptr(*target.Likeable), } return boost, nil diff --git a/internal/typeutils/internaltorss_test.go b/internal/typeutils/internaltorss_test.go index e9848b806..0988b8ecb 100644 --- a/internal/typeutils/internaltorss_test.go +++ b/internal/typeutils/internaltorss_test.go @@ -98,9 +98,6 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() { Visibility: gtsmodel.VisibilityDefault, ActivityStreamsType: ap.ObjectNote, Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), } item, err := suite.typeconverter.StatusToRSSItem(context.Background(), s) suite.NoError(err) diff --git a/test/envparsing.sh b/test/envparsing.sh index a744717a4..29403011e 100755 --- a/test/envparsing.sh +++ b/test/envparsing.sh @@ -43,6 +43,7 @@ EXPECT=$(cat << "EOF" "follow-request-mem-ratio": 2, "in-reply-to-ids-mem-ratio": 3, "instance-mem-ratio": 1, + "interaction-approval-mem-ratio": 1, "list-entry-mem-ratio": 2, "list-mem-ratio": 1, "marker-mem-ratio": 0.5, diff --git a/testrig/testmodels.go b/testrig/testmodels.go index de6e97142..90c200585 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -1401,9 +1401,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "admin_account_status_2": { @@ -1427,9 +1424,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "admin_account_status_3": { @@ -1454,9 +1448,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "admin_account_status_4": { @@ -1482,9 +1473,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_1_status_1": { @@ -1507,9 +1495,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_1_status_2": { @@ -1532,9 +1517,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(false), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_1_status_3": { @@ -1557,10 +1539,18 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), - Boostable: util.Ptr(false), - Replyable: util.Ptr(false), - Likeable: util.Ptr(false), - ActivityStreamsType: ap.ObjectNote, + InteractionPolicy: >smodel.InteractionPolicy{ + CanLike: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, + }, + CanReply: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, + }, + CanAnnounce: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, + }, + }, + ActivityStreamsType: ap.ObjectNote, }, "local_account_1_status_4": { ID: "01F8MH82FYRXD2RC6108DAJ5HB", @@ -1583,9 +1573,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_1_status_5": { @@ -1609,9 +1596,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_1_status_6": { @@ -1635,9 +1619,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEN2RKT1YTEZ80SA8HGP105F", }, @@ -1661,9 +1642,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_1": { @@ -1686,9 +1664,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_2": { @@ -1711,17 +1686,25 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(false), - Likeable: util.Ptr(true), - ActivityStreamsType: ap.ObjectNote, + InteractionPolicy: >smodel.InteractionPolicy{ + CanLike: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, + }, + CanReply: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, + }, + CanAnnounce: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, + }, + }, + ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_3": { ID: "01F8MHC8VWDRBQR0N1BATDDEM5", URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5", URL: "http://localhost:8080/@1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5", - Content: "🐢 i don't mind people sharing this one but I don't want likes or replies to it because cba🐢", - Text: "🐢 i don't mind people sharing this one but I don't want likes or replies to it because cba🐢", + Content: "🐢 i don't mind people sharing and liking this one but I want to moderate replies to it 🐢", + Text: "🐢 i don't mind people sharing and liking this one but I want to moderate replies to it 🐢", CreatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"), UpdatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"), Local: util.Ptr(true), @@ -1730,16 +1713,25 @@ func NewTestStatuses() map[string]*gtsmodel.Status { InReplyToID: "", BoostOfID: "", ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0", - ContentWarning: "you won't be able to like or reply to this", + ContentWarning: "you won't be able to reply to this without my approval", Visibility: gtsmodel.VisibilityUnlocked, Sensitive: util.Ptr(true), Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(false), - Likeable: util.Ptr(false), - ActivityStreamsType: ap.ObjectNote, + InteractionPolicy: >smodel.InteractionPolicy{ + CanLike: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, + }, + CanReply: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, + WithApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, + }, + CanAnnounce: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, + }, + }, + ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_4": { ID: "01F8MHCP5P2NWYQ416SBA0XSEV", @@ -1761,10 +1753,17 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(false), - Boostable: util.Ptr(false), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), - + InteractionPolicy: >smodel.InteractionPolicy{ + CanLike: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, + }, + CanReply: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, + }, + CanAnnounce: gtsmodel.PolicyRules{ + Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, + }, + }, ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_5": { @@ -1790,9 +1789,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_6": { @@ -1818,9 +1814,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_7": { @@ -1845,9 +1838,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "local_account_2_status_8": { @@ -1871,9 +1861,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEN2QB5NR4NCEHGYC3HN84K6", }, @@ -1899,9 +1886,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, "remote_account_1_status_2": { @@ -1925,9 +1909,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEN2R65468ZG657C4ZPHJ4EX", }, @@ -1952,9 +1933,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ActivityQuestion, PollID: "01HEWV1GW2D49R919NPEDXPTZ5", }, @@ -1980,9 +1958,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Language: "en", CreatedWithApplicationID: "", Federated: util.Ptr(true), - Boostable: util.Ptr(true), - Replyable: util.Ptr(true), - Likeable: util.Ptr(true), ActivityStreamsType: ap.ObjectNote, }, }