[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
This commit is contained in:
tobi 2024-07-11 16:44:29 +02:00 committed by GitHub
parent 8f8093aea4
commit 5bc567196b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1318 additions and 531 deletions

View file

@ -8017,21 +8017,6 @@ paths:
name: federated name: federated
type: boolean type: boolean
x-go-name: Federated 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: produces:
- application/json - application/json
responses: responses:

View file

@ -173,42 +173,43 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
} }
// try to boost a status that's not boostable / visible to us // try to boost a status that's not boostable / visible to us
func (suite *StatusBoostTestSuite) TestPostUnboostable() { // TODO: sort this out with new interaction policies
t := suite.testTokens["local_account_1"] // func (suite *StatusBoostTestSuite) TestPostUnboostable() {
oauthToken := oauth.DBTokenToToken(t) // 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 // // setup
recorder := httptest.NewRecorder() // recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil) // ctx, _ := testrig.CreateGinTestContext(recorder, nil)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) // ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) // ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) // ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) // ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting // ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
ctx.Request.Header.Set("accept", "application/json") // ctx.Request.Header.Set("accept", "application/json")
// normally the router would populate these params from the path values, // // normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually. // // but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{ // ctx.Params = gin.Params{
gin.Param{ // gin.Param{
Key: statuses.IDKey, // Key: statuses.IDKey,
Value: targetStatus.ID, // Value: targetStatus.ID,
}, // },
} // }
suite.statusModule.StatusBoostPOSTHandler(ctx) // suite.statusModule.StatusBoostPOSTHandler(ctx)
// check response // // check response
suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses // suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses
result := recorder.Result() // result := recorder.Result()
defer result.Body.Close() // defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) // b, err := ioutil.ReadAll(result.Body)
suite.NoError(err) // suite.NoError(err)
suite.Equal(`{"error":"Not Found"}`, string(b)) // suite.Equal(`{"error":"Not Found"}`, string(b))
} // }
// try to boost a status that's not visible to the user // try to boost a status that's not visible to the user
func (suite *StatusBoostTestSuite) TestPostNotVisible() { func (suite *StatusBoostTestSuite) TestPostNotVisible() {

View file

@ -168,24 +168,6 @@ import (
// description: This status will be federated beyond the local timeline(s). // description: This status will be federated beyond the local timeline(s).
// in: formData // in: formData
// type: boolean // 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: // produces:
// - application/json // - application/json

View file

@ -67,9 +67,6 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
"spoiler_text": {"hello hello"}, "spoiler_text": {"hello hello"},
"sensitive": {"true"}, "sensitive": {"true"},
"visibility": {string(apimodel.VisibilityMutualsOnly)}, "visibility": {string(apimodel.VisibilityMutualsOnly)},
"likeable": {"false"},
"replyable": {"false"},
"federated": {"false"},
} }
suite.statusModule.StatusCreatePOSTHandler(ctx) suite.statusModule.StatusCreatePOSTHandler(ctx)

View file

@ -89,42 +89,43 @@ func (suite *StatusFaveTestSuite) TestPostFave() {
} }
// try to fave a status that's not faveable // try to fave a status that's not faveable
func (suite *StatusFaveTestSuite) TestPostUnfaveable() { // TODO: replace this when interaction policies enforced.
t := suite.testTokens["local_account_1"] // func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
oauthToken := oauth.DBTokenToToken(t) // 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 // // setup
recorder := httptest.NewRecorder() // recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil) // ctx, _ := testrig.CreateGinTestContext(recorder, nil)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) // ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) // ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) // ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) // ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting // ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
ctx.Request.Header.Set("accept", "application/json") // ctx.Request.Header.Set("accept", "application/json")
// normally the router would populate these params from the path values, // // normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually. // // but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{ // ctx.Params = gin.Params{
gin.Param{ // gin.Param{
Key: statuses.IDKey, // Key: statuses.IDKey,
Value: targetStatus.ID, // Value: targetStatus.ID,
}, // },
} // }
suite.statusModule.StatusFavePOSTHandler(ctx) // suite.statusModule.StatusFavePOSTHandler(ctx)
// check response // // check response
suite.EqualValues(http.StatusForbidden, recorder.Code) // suite.EqualValues(http.StatusForbidden, recorder.Code)
result := recorder.Result() // result := recorder.Result()
defer result.Body.Close() // defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) // b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) // assert.NoError(suite.T(), err)
assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b)) // assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b))
} // }
func TestStatusFaveTestSuite(t *testing.T) { func TestStatusFaveTestSuite(t *testing.T) {
suite.Run(t, new(StatusFaveTestSuite)) suite.Run(t, new(StatusFaveTestSuite))

View file

@ -183,9 +183,6 @@ func (suite *StatusPinTestSuite) TestPinStatusTooManyPins() {
AccountURI: testAccount.URI, AccountURI: testAccount.URI,
Visibility: gtsmodel.VisibilityPublic, Visibility: gtsmodel.VisibilityPublic,
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, status); err != nil { if err := suite.db.PutStatus(ctx, status); err != nil {

View file

@ -230,12 +230,6 @@ type AdvancedStatusCreateForm struct {
type AdvancedVisibilityFlagsForm struct { type AdvancedVisibilityFlagsForm struct {
// This status will be federated beyond the local timeline(s). // This status will be federated beyond the local timeline(s).
Federated *bool `form:"federated" json:"federated" xml:"federated"` 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. // StatusContentType is the content type with which to parse the submitted status.

View file

@ -73,6 +73,7 @@ func (c *Caches) Init() {
c.initFollowRequestIDs() c.initFollowRequestIDs()
c.initInReplyToIDs() c.initInReplyToIDs()
c.initInstance() c.initInstance()
c.initInteractionApproval()
c.initList() c.initList()
c.initListEntry() c.initListEntry()
c.initMarker() c.initMarker()
@ -145,6 +146,7 @@ func (c *Caches) Sweep(threshold float64) {
c.GTS.FollowRequestIDs.Trim(threshold) c.GTS.FollowRequestIDs.Trim(threshold)
c.GTS.InReplyToIDs.Trim(threshold) c.GTS.InReplyToIDs.Trim(threshold)
c.GTS.Instance.Trim(threshold) c.GTS.Instance.Trim(threshold)
c.GTS.InteractionApproval.Trim(threshold)
c.GTS.List.Trim(threshold) c.GTS.List.Trim(threshold)
c.GTS.ListEntry.Trim(threshold) c.GTS.ListEntry.Trim(threshold)
c.GTS.Marker.Trim(threshold) c.GTS.Marker.Trim(threshold)

37
internal/cache/db.go vendored
View file

@ -100,6 +100,9 @@ type GTSCaches struct {
// Instance provides access to the gtsmodel Instance database cache. // Instance provides access to the gtsmodel Instance database cache.
Instance StructCache[*gtsmodel.Instance] 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 provides access to the status in reply to IDs list database cache.
InReplyToIDs SliceCache[string] 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() { func (c *Caches) initList() {
// Calculate maximum cache size. // Calculate maximum cache size.
cap := calculateResultCacheMax( cap := calculateResultCacheMax(
@ -1188,6 +1224,7 @@ func (c *Caches) initStatusFave() {
c.GTS.StatusFave.Init(structr.CacheConfig[*gtsmodel.StatusFave]{ c.GTS.StatusFave.Init(structr.CacheConfig[*gtsmodel.StatusFave]{
Indices: []structr.IndexConfig{ Indices: []structr.IndexConfig{
{Fields: "ID"}, {Fields: "ID"},
{Fields: "URI"},
{Fields: "AccountID,StatusID"}, {Fields: "AccountID,StatusID"},
{Fields: "StatusID", Multiple: true}, {Fields: "StatusID", Multiple: true},
}, },

View file

@ -189,6 +189,7 @@ func totalOfRatios() float64 {
config.GetCacheFollowRequestMemRatio() + config.GetCacheFollowRequestMemRatio() +
config.GetCacheFollowRequestIDsMemRatio() + config.GetCacheFollowRequestIDsMemRatio() +
config.GetCacheInstanceMemRatio() + config.GetCacheInstanceMemRatio() +
config.GetCacheInteractionApprovalMemRatio() +
config.GetCacheInReplyToIDsMemRatio() + config.GetCacheInReplyToIDsMemRatio() +
config.GetCacheListMemRatio() + config.GetCacheListMemRatio() +
config.GetCacheListEntryMemRatio() + config.GetCacheListEntryMemRatio() +
@ -425,6 +426,19 @@ func sizeofInstance() uintptr {
})) }))
} }
func sizeofInteractionApproval() uintptr {
return uintptr(size.Of(&gtsmodel.InteractionApproval{
ID: exampleID,
CreatedAt: exampleTime,
UpdatedAt: exampleTime,
AccountID: exampleID,
InteractingAccountID: exampleID,
InteractionURI: exampleURI,
InteractionType: gtsmodel.InteractionAnnounce,
URI: exampleURI,
}))
}
func sizeofList() uintptr { func sizeofList() uintptr {
return uintptr(size.Of(&gtsmodel.List{ return uintptr(size.Of(&gtsmodel.List{
ID: exampleID, ID: exampleID,
@ -591,9 +605,6 @@ func sizeofStatus() uintptr {
Language: "en", Language: "en",
CreatedWithApplicationID: exampleID, CreatedWithApplicationID: exampleID,
Federated: func() *bool { ok := true; return &ok }(), 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, ActivityStreamsType: ap.ObjectNote,
})) }))
} }

View file

@ -191,52 +191,53 @@ type HTTPClientConfiguration struct {
} }
type CacheConfiguration struct { type CacheConfiguration struct {
MemoryTarget bytesize.Size `name:"memory-target"` MemoryTarget bytesize.Size `name:"memory-target"`
AccountMemRatio float64 `name:"account-mem-ratio"` AccountMemRatio float64 `name:"account-mem-ratio"`
AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"` AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"`
AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"` AccountStatsMemRatio float64 `name:"account-stats-mem-ratio"`
ApplicationMemRatio float64 `name:"application-mem-ratio"` ApplicationMemRatio float64 `name:"application-mem-ratio"`
BlockMemRatio float64 `name:"block-mem-ratio"` BlockMemRatio float64 `name:"block-mem-ratio"`
BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"` BlockIDsMemRatio float64 `name:"block-ids-mem-ratio"`
BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"` BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"`
ClientMemRatio float64 `name:"client-mem-ratio"` ClientMemRatio float64 `name:"client-mem-ratio"`
EmojiMemRatio float64 `name:"emoji-mem-ratio"` EmojiMemRatio float64 `name:"emoji-mem-ratio"`
EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"` EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"`
FilterMemRatio float64 `name:"filter-mem-ratio"` FilterMemRatio float64 `name:"filter-mem-ratio"`
FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"` FilterKeywordMemRatio float64 `name:"filter-keyword-mem-ratio"`
FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"` FilterStatusMemRatio float64 `name:"filter-status-mem-ratio"`
FollowMemRatio float64 `name:"follow-mem-ratio"` FollowMemRatio float64 `name:"follow-mem-ratio"`
FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"` FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"`
FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"` FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"`
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"` FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"` InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
InstanceMemRatio float64 `name:"instance-mem-ratio"` InstanceMemRatio float64 `name:"instance-mem-ratio"`
ListMemRatio float64 `name:"list-mem-ratio"` InteractionApprovalMemRatio float64 `name:"interaction-approval-mem-ratio"`
ListEntryMemRatio float64 `name:"list-entry-mem-ratio"` ListMemRatio float64 `name:"list-mem-ratio"`
MarkerMemRatio float64 `name:"marker-mem-ratio"` ListEntryMemRatio float64 `name:"list-entry-mem-ratio"`
MediaMemRatio float64 `name:"media-mem-ratio"` MarkerMemRatio float64 `name:"marker-mem-ratio"`
MentionMemRatio float64 `name:"mention-mem-ratio"` MediaMemRatio float64 `name:"media-mem-ratio"`
MoveMemRatio float64 `name:"move-mem-ratio"` MentionMemRatio float64 `name:"mention-mem-ratio"`
NotificationMemRatio float64 `name:"notification-mem-ratio"` MoveMemRatio float64 `name:"move-mem-ratio"`
PollMemRatio float64 `name:"poll-mem-ratio"` NotificationMemRatio float64 `name:"notification-mem-ratio"`
PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"` PollMemRatio float64 `name:"poll-mem-ratio"`
PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"` PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"`
ReportMemRatio float64 `name:"report-mem-ratio"` PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"`
StatusMemRatio float64 `name:"status-mem-ratio"` ReportMemRatio float64 `name:"report-mem-ratio"`
StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"` StatusMemRatio float64 `name:"status-mem-ratio"`
StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"` StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"`
StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"` StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"`
StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"` StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
TagMemRatio float64 `name:"tag-mem-ratio"` StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"` TagMemRatio float64 `name:"tag-mem-ratio"`
TokenMemRatio float64 `name:"token-mem-ratio"` ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"`
TombstoneMemRatio float64 `name:"tombstone-mem-ratio"` TokenMemRatio float64 `name:"token-mem-ratio"`
UserMemRatio float64 `name:"user-mem-ratio"` TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
UserMuteMemRatio float64 `name:"user-mute-mem-ratio"` UserMemRatio float64 `name:"user-mem-ratio"`
UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"` UserMuteMemRatio float64 `name:"user-mute-mem-ratio"`
WebfingerMemRatio float64 `name:"webfinger-mem-ratio"` UserMuteIDsMemRatio float64 `name:"user-mute-ids-mem-ratio"`
VisibilityMemRatio float64 `name:"visibility-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). // MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML).

View file

@ -156,51 +156,52 @@ var Defaults = Configuration{
// when TODO items in the size.go source // when TODO items in the size.go source
// file have been addressed, these should // file have been addressed, these should
// be able to make some more sense :D // be able to make some more sense :D
AccountMemRatio: 5, AccountMemRatio: 5,
AccountNoteMemRatio: 1, AccountNoteMemRatio: 1,
AccountSettingsMemRatio: 0.1, AccountSettingsMemRatio: 0.1,
AccountStatsMemRatio: 2, AccountStatsMemRatio: 2,
ApplicationMemRatio: 0.1, ApplicationMemRatio: 0.1,
BlockMemRatio: 2, BlockMemRatio: 2,
BlockIDsMemRatio: 3, BlockIDsMemRatio: 3,
BoostOfIDsMemRatio: 3, BoostOfIDsMemRatio: 3,
ClientMemRatio: 0.1, ClientMemRatio: 0.1,
EmojiMemRatio: 3, EmojiMemRatio: 3,
EmojiCategoryMemRatio: 0.1, EmojiCategoryMemRatio: 0.1,
FilterMemRatio: 0.5, FilterMemRatio: 0.5,
FilterKeywordMemRatio: 0.5, FilterKeywordMemRatio: 0.5,
FilterStatusMemRatio: 0.5, FilterStatusMemRatio: 0.5,
FollowMemRatio: 2, FollowMemRatio: 2,
FollowIDsMemRatio: 4, FollowIDsMemRatio: 4,
FollowRequestMemRatio: 2, FollowRequestMemRatio: 2,
FollowRequestIDsMemRatio: 2, FollowRequestIDsMemRatio: 2,
InReplyToIDsMemRatio: 3, InReplyToIDsMemRatio: 3,
InstanceMemRatio: 1, InstanceMemRatio: 1,
ListMemRatio: 1, InteractionApprovalMemRatio: 1,
ListEntryMemRatio: 2, ListMemRatio: 1,
MarkerMemRatio: 0.5, ListEntryMemRatio: 2,
MediaMemRatio: 4, MarkerMemRatio: 0.5,
MentionMemRatio: 2, MediaMemRatio: 4,
MoveMemRatio: 0.1, MentionMemRatio: 2,
NotificationMemRatio: 2, MoveMemRatio: 0.1,
PollMemRatio: 1, NotificationMemRatio: 2,
PollVoteMemRatio: 2, PollMemRatio: 1,
PollVoteIDsMemRatio: 2, PollVoteMemRatio: 2,
ReportMemRatio: 1, PollVoteIDsMemRatio: 2,
StatusMemRatio: 5, ReportMemRatio: 1,
StatusBookmarkMemRatio: 0.5, StatusMemRatio: 5,
StatusBookmarkIDsMemRatio: 2, StatusBookmarkMemRatio: 0.5,
StatusFaveMemRatio: 2, StatusBookmarkIDsMemRatio: 2,
StatusFaveIDsMemRatio: 3, StatusFaveMemRatio: 2,
TagMemRatio: 2, StatusFaveIDsMemRatio: 3,
ThreadMuteMemRatio: 0.2, TagMemRatio: 2,
TokenMemRatio: 0.75, ThreadMuteMemRatio: 0.2,
TombstoneMemRatio: 0.5, TokenMemRatio: 0.75,
UserMemRatio: 0.25, TombstoneMemRatio: 0.5,
UserMuteMemRatio: 2, UserMemRatio: 0.25,
UserMuteIDsMemRatio: 3, UserMuteMemRatio: 2,
WebfingerMemRatio: 0.1, UserMuteIDsMemRatio: 3,
VisibilityMemRatio: 2, WebfingerMemRatio: 0.1,
VisibilityMemRatio: 2,
}, },
HTTPClient: HTTPClientConfiguration{ HTTPClient: HTTPClientConfiguration{

View file

@ -3250,6 +3250,33 @@ func GetCacheInstanceMemRatio() float64 { return global.GetCacheInstanceMemRatio
// SetCacheInstanceMemRatio safely sets the value for global configuration 'Cache.InstanceMemRatio' field // SetCacheInstanceMemRatio safely sets the value for global configuration 'Cache.InstanceMemRatio' field
func SetCacheInstanceMemRatio(v float64) { global.SetCacheInstanceMemRatio(v) } 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 // GetCacheListMemRatio safely fetches the Configuration value for state's 'Cache.ListMemRatio' field
func (st *ConfigState) GetCacheListMemRatio() (v float64) { func (st *ConfigState) GetCacheListMemRatio() (v float64) {
st.mutex.RLock() st.mutex.RLock()

View file

@ -115,15 +115,6 @@ func (suite *AccountTestSuite) populateTestStatus(testAccountKey string, status
if status.Federated == nil { if status.Federated == nil {
status.Federated = util.Ptr(true) 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 { if inReplyTo != nil {
status.InReplyToAccountID = inReplyTo.AccountID status.InReplyToAccountID = inReplyTo.AccountID

View file

@ -60,6 +60,7 @@ type DBService struct {
db.Emoji db.Emoji
db.HeaderFilter db.HeaderFilter
db.Instance db.Instance
db.Interaction
db.Filter db.Filter
db.List db.List
db.Marker db.Marker
@ -203,6 +204,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
db: db, db: db,
state: state, state: state,
}, },
Interaction: &interactionDB{
db: db,
state: state,
},
Filter: &filterDB{ Filter: &filterDB{
db: db, db: db,
state: state, state: state,

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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 := &gtsmodel.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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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"`
}

View file

@ -44,9 +44,6 @@ func (suite *StatusTestSuite) TestGetStatusByID() {
suite.Nil(status.InReplyTo) suite.Nil(status.InReplyTo)
suite.Nil(status.InReplyToAccount) suite.Nil(status.InReplyToAccount)
suite.True(*status.Federated) suite.True(*status.Federated)
suite.True(*status.Boostable)
suite.True(*status.Replyable)
suite.True(*status.Likeable)
} }
func (suite *StatusTestSuite) TestGetStatusesByIDs() { func (suite *StatusTestSuite) TestGetStatusesByIDs() {
@ -73,9 +70,6 @@ func (suite *StatusTestSuite) TestGetStatusesByIDs() {
suite.Nil(status1.InReplyTo) suite.Nil(status1.InReplyTo)
suite.Nil(status1.InReplyToAccount) suite.Nil(status1.InReplyToAccount)
suite.True(*status1.Federated) suite.True(*status1.Federated)
suite.True(*status1.Boostable)
suite.True(*status1.Replyable)
suite.True(*status1.Likeable)
status2 := statuses[1] status2 := statuses[1]
suite.NotNil(status2) suite.NotNil(status2)
@ -86,9 +80,6 @@ func (suite *StatusTestSuite) TestGetStatusesByIDs() {
suite.Nil(status2.InReplyTo) suite.Nil(status2.InReplyTo)
suite.Nil(status2.InReplyToAccount) suite.Nil(status2.InReplyToAccount)
suite.True(*status2.Federated) suite.True(*status2.Federated)
suite.True(*status2.Boostable)
suite.False(*status2.Replyable)
suite.False(*status2.Likeable)
} }
func (suite *StatusTestSuite) TestGetStatusByURI() { func (suite *StatusTestSuite) TestGetStatusByURI() {
@ -104,9 +95,6 @@ func (suite *StatusTestSuite) TestGetStatusByURI() {
suite.Nil(status.InReplyTo) suite.Nil(status.InReplyTo)
suite.Nil(status.InReplyToAccount) suite.Nil(status.InReplyToAccount)
suite.True(*status.Federated) suite.True(*status.Federated)
suite.True(*status.Boostable)
suite.False(*status.Replyable)
suite.False(*status.Likeable)
} }
func (suite *StatusTestSuite) TestGetStatusWithExtras() { func (suite *StatusTestSuite) TestGetStatusWithExtras() {
@ -121,9 +109,6 @@ func (suite *StatusTestSuite) TestGetStatusWithExtras() {
suite.NotEmpty(status.Attachments) suite.NotEmpty(status.Attachments)
suite.NotEmpty(status.Emojis) suite.NotEmpty(status.Emojis)
suite.True(*status.Federated) suite.True(*status.Federated)
suite.True(*status.Boostable)
suite.True(*status.Replyable)
suite.True(*status.Likeable)
} }
func (suite *StatusTestSuite) TestGetStatusWithMention() { func (suite *StatusTestSuite) TestGetStatusWithMention() {
@ -138,9 +123,6 @@ func (suite *StatusTestSuite) TestGetStatusWithMention() {
suite.NotEmpty(status.InReplyToID) suite.NotEmpty(status.InReplyToID)
suite.NotEmpty(status.InReplyToAccountID) suite.NotEmpty(status.InReplyToAccountID)
suite.True(*status.Federated) 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 // The below test was originally used to ensure that a second

View file

@ -23,6 +23,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"slices" "slices"
"time"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext" "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) { 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 // Fetch status fave from database cache with loader callback
fave, err := s.state.Caches.GTS.StatusFave.LoadOne(lookup, func() (*gtsmodel.StatusFave, error) { 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 { func (s *statusFaveDB) DeleteStatusFaveByID(ctx context.Context, id string) error {
var statusID string var statusID string

View file

@ -65,9 +65,7 @@ func getFutureStatus() *gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true), InteractionPolicy: gtsmodel.DefaultInteractionPolicyPublic(),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
} }

View file

@ -32,6 +32,7 @@ type DB interface {
Emoji Emoji
HeaderFilter HeaderFilter
Instance Instance
Interaction
Filter Filter
List List
Marker Marker

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -27,9 +27,12 @@ type StatusFave interface {
// GetStatusFaveByAccountID gets one status fave created by the given accountID, targeting the given statusID. // 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(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) 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. // 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. // 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) 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 inserts the given statusFave into the database.
PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error 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. // DeleteStatusFave deletes one status fave with the given id.
DeleteStatusFaveByID(ctx context.Context, id string) error DeleteStatusFaveByID(ctx context.Context, id string) error

View file

@ -92,9 +92,6 @@ func (d *Dereferencer) EnrichAnnounce(
boost.BoostOfAccount = target.Account boost.BoostOfAccount = target.Account
boost.Visibility = target.Visibility boost.Visibility = target.Visibility
boost.Federated = target.Federated boost.Federated = target.Federated
boost.Boostable = target.Boostable
boost.Replyable = target.Replyable
boost.Likeable = target.Likeable
// Store the boost wrapper status in database. // Store the boost wrapper status in database.
switch err = d.state.DB.PutStatus(ctx, boost); { switch err = d.state.DB.PutStatus(ctx, boost); {

View file

@ -633,9 +633,7 @@ func (d *Dereferencer) isPermittedStatus(
} }
} }
if permitted && if permitted {
*status.InReplyTo.Replyable {
// Status is reply-able to.
return true, nil return true, nil
} }

View file

@ -56,9 +56,6 @@ func (suite *StatusTestSuite) TestDereferenceSimpleStatus() {
suite.NoError(err) suite.NoError(err)
suite.Equal(status.ID, dbStatus.ID) suite.Equal(status.ID, dbStatus.ID)
suite.True(*dbStatus.Federated) 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 should be in the database now too
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)
@ -96,9 +93,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithMention() {
suite.NoError(err) suite.NoError(err)
suite.Equal(status.ID, dbStatus.ID) suite.Equal(status.ID, dbStatus.ID)
suite.True(*dbStatus.Federated) 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 should be in the database now too
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)
@ -151,9 +145,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithTag() {
suite.NoError(err) suite.NoError(err)
suite.Equal(status.ID, dbStatus.ID) suite.Equal(status.ID, dbStatus.ID)
suite.True(*dbStatus.Federated) 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 should be in the database now too
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)
@ -197,9 +188,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithImageAndNoContent() {
suite.NoError(err) suite.NoError(err)
suite.Equal(status.ID, dbStatus.ID) suite.Equal(status.ID, dbStatus.ID)
suite.True(*dbStatus.Federated) 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 should be in the database now too
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI) account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)

View file

@ -53,10 +53,5 @@ func (f *Filter) StatusBoostable(ctx context.Context, requester *gtsmodel.Accoun
return false, nil return false, nil
} }
if !*status.Boostable {
log.Trace(ctx, "status marked not boostable")
return false, nil
}
return true, nil return true, nil
} }

View file

@ -161,9 +161,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestThread() {
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
@ -214,9 +211,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, originalStatus); err != nil { if err := suite.db.PutStatus(ctx, originalStatus); err != nil {
@ -248,9 +242,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
@ -282,9 +273,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil { if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil {
@ -327,9 +315,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, originalStatus); err != nil { if err := suite.db.PutStatus(ctx, originalStatus); err != nil {
@ -361,9 +346,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil { if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
@ -395,9 +377,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
} }
if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil { if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil {

View file

@ -21,15 +21,20 @@ import "time"
// AccountSettings models settings / preferences for a local, non-instance account. // AccountSettings models settings / preferences for a local, non-instance account.
type AccountSettings struct { type AccountSettings struct {
AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings. 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. 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. 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 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? 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? 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). 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). 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. 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 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. 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.
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
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
)

View file

@ -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 <http://www.gnu.org/licenses/>.
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,
},
}
}

View file

@ -39,12 +39,15 @@ type NotificationType string
// Notification Types // Notification Types
const ( const (
NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you NotificationFollow NotificationType = "follow" // NotificationFollow -- someone followed you
NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you NotificationFollowRequest NotificationType = "follow_request" // NotificationFollowRequest -- someone requested to follow you
NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status NotificationMention NotificationType = "mention" // NotificationMention -- someone mentioned you in their status
NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses NotificationReblog NotificationType = "reblog" // NotificationReblog -- someone boosted one of your statuses
NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked 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 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. 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. 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.
) )

View file

@ -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!. 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 Text string `bun:""` // Original text of the status without formatting
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s) Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
Boostable *bool `bun:",notnull"` // This status can be boosted/reblogged 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.
Replyable *bool `bun:",notnull"` // This status can be replied to 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.
Likeable *bool `bun:",notnull"` // This status can be liked/faved 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{}. // GetID implements timeline.Timelineable{}.

View file

@ -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' 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 Status *Status `bun:"-"` // the faved status
URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI of this fave 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.
} }

View file

@ -185,11 +185,6 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac
return errWithCode 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. // Set status fields from inReplyTo.
status.InReplyToID = inReplyTo.ID status.InReplyToID = inReplyTo.ID
status.InReplyTo = inReplyTo 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 { func processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
// by default all flags are set to true // by default all flags are set to true
federated := true federated := true
boostable := true
replyable := true
likeable := true
// If visibility isn't set on the form, then just take the account default. // 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. // 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 vis = gtsmodel.VisibilityDefault
} }
switch vis { // Todo: sort out likeable/replyable/boostable in next PR.
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
}
status.Visibility = vis status.Visibility = vis
status.Federated = &federated status.Federated = &federated
status.Boostable = &boostable
status.Replyable = &replyable
status.Likeable = &likeable
return nil return nil
} }

View file

@ -53,9 +53,6 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks(
}, },
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil, Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
}, },
} }
@ -87,9 +84,6 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuot
}, },
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil, Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
}, },
} }
@ -125,9 +119,6 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji
}, },
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil, Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
}, },
} }
@ -159,9 +150,6 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj
}, },
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil, Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
}, },
} }
@ -197,9 +185,6 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
}, },
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil, Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
}, },
} }
@ -229,9 +214,6 @@ func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() {
}, },
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil, Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
}, },
} }
@ -266,9 +248,6 @@ func (suite *StatusCreateTestSuite) TestProcessReplyToUnthreadedRemoteStatus() {
}, },
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
Federated: nil, Federated: nil,
Boostable: nil,
Replyable: nil,
Likeable: nil,
}, },
} }

View file

@ -62,11 +62,6 @@ func (p *Processor) getFaveableStatus(
return nil, nil, errWithCode 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) fave, err := p.state.DB.GetStatusFave(ctx, requester.ID, target.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err) err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)

View file

@ -69,9 +69,6 @@ func (suite *FromClientAPITestSuite) newStatus(
Visibility: visibility, Visibility: visibility,
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
} }
if replyToStatus != nil { if replyToStatus != nil {

View file

@ -89,77 +89,78 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
suite.False(*notif.Read) suite.False(*notif.Read)
} }
func (suite *FromFediAPITestSuite) TestProcessReplyMention() { // Todo: fix this test up in interaction policies PR.
testStructs := suite.SetupTestStructs() // func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
defer suite.TearDownTestStructs(testStructs) // testStructs := suite.SetupTestStructs()
// defer suite.TearDownTestStructs(testStructs)
repliedAccount := suite.testAccounts["local_account_1"] // repliedAccount := suite.testAccounts["local_account_1"]
repliedStatus := suite.testStatuses["local_account_1_status_1"] // repliedStatus := suite.testStatuses["local_account_1_status_1"]
replyingAccount := suite.testAccounts["remote_account_1"] // replyingAccount := suite.testAccounts["remote_account_1"]
// Set the replyingAccount's last fetched_at // // Set the replyingAccount's last fetched_at
// date to something recent so no refresh is attempted, // // date to something recent so no refresh is attempted,
// and ensure it isn't a suspended account. // // and ensure it isn't a suspended account.
replyingAccount.FetchedAt = time.Now() // replyingAccount.FetchedAt = time.Now()
replyingAccount.SuspendedAt = time.Time{} // replyingAccount.SuspendedAt = time.Time{}
replyingAccount.SuspensionOrigin = "" // replyingAccount.SuspensionOrigin = ""
err := testStructs.State.DB.UpdateAccount(context.Background(), // err := testStructs.State.DB.UpdateAccount(context.Background(),
replyingAccount, // replyingAccount,
"fetched_at", // "fetched_at",
"suspended_at", // "suspended_at",
"suspension_origin", // "suspension_origin",
) // )
suite.NoError(err) // suite.NoError(err)
// Get replying statusable to use from remote test statuses. // // Get replying statusable to use from remote test statuses.
const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552" // const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552"
replyingStatusable := testrig.NewTestFediStatuses()[replyingURI] // replyingStatusable := testrig.NewTestFediStatuses()[replyingURI]
ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI)) // ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI))
// Open a websocket stream to later test the streamed status reply. // // Open a websocket stream to later test the streamed status reply.
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) // wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
suite.NoError(errWithCode) // suite.NoError(errWithCode)
// Send the replied status off to the fedi worker to be further processed. // // Send the replied status off to the fedi worker to be further processed.
err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{ // err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
APObjectType: ap.ObjectNote, // APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate, // APActivityType: ap.ActivityCreate,
APObject: replyingStatusable, // APObject: replyingStatusable,
Receiving: repliedAccount, // Receiving: repliedAccount,
Requesting: replyingAccount, // Requesting: replyingAccount,
}) // })
suite.NoError(err) // suite.NoError(err)
// side effects should be triggered // // side effects should be triggered
// 1. status should be in the database // // 1. status should be in the database
replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI) // replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI)
suite.NoError(err) // suite.NoError(err)
// 2. a notification should exist for the mention // // 2. a notification should exist for the mention
var notif gtsmodel.Notification // var notif gtsmodel.Notification
err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{ // err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{
{Key: "status_id", Value: replyingStatus.ID}, // {Key: "status_id", Value: replyingStatus.ID},
}, &notif) // }, &notif)
suite.NoError(err) // suite.NoError(err)
suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) // suite.Equal(gtsmodel.NotificationMention, notif.NotificationType)
suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) // suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID)
suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) // suite.Equal(replyingStatus.AccountID, notif.OriginAccountID)
suite.Equal(replyingStatus.ID, notif.StatusID) // suite.Equal(replyingStatus.ID, notif.StatusID)
suite.False(*notif.Read) // suite.False(*notif.Read)
ctx, _ := context.WithTimeout(context.Background(), time.Second*5) // ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
msg, ok := wssStream.Recv(ctx) // msg, ok := wssStream.Recv(ctx)
suite.True(ok) // suite.True(ok)
suite.Equal(stream.EventTypeNotification, msg.Event) // suite.Equal(stream.EventTypeNotification, msg.Event)
suite.NotEmpty(msg.Payload) // suite.NotEmpty(msg.Payload)
suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) // suite.EqualValues([]string{stream.TimelineHome}, msg.Stream)
notifStreamed := &apimodel.Notification{} // notifStreamed := &apimodel.Notification{}
err = json.Unmarshal([]byte(msg.Payload), notifStreamed) // err = json.Unmarshal([]byte(msg.Payload), notifStreamed)
suite.NoError(err) // suite.NoError(err)
suite.Equal("mention", notifStreamed.Type) // suite.Equal("mention", notifStreamed.Type)
suite.Equal(replyingAccount.ID, notifStreamed.Account.ID) // suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
} // }
func (suite *FromFediAPITestSuite) TestProcessFave() { func (suite *FromFediAPITestSuite) TestProcessFave() {
testStructs := suite.SetupTestStructs() testStructs := suite.SetupTestStructs()

View file

@ -399,9 +399,6 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// needs to be created for this in go-fed/activity. // needs to be created for this in go-fed/activity.
// Until this is implemented, assume all true. // Until this is implemented, assume all true.
status.Federated = util.Ptr(true) status.Federated = util.Ptr(true)
status.Boostable = util.Ptr(true)
status.Replyable = util.Ptr(true)
status.Likeable = util.Ptr(true)
// status.Sensitive // status.Sensitive
sensitive := ap.ExtractSensitive(statusable) sensitive := ap.ExtractSensitive(statusable)

View file

@ -181,9 +181,6 @@ func (suite *ASToInternalTestSuite) TestParseReplyWithMention() {
suite.Equal(inReplyToStatus.ID, status.InReplyToID) suite.Equal(inReplyToStatus.ID, status.InReplyToID)
suite.Equal(inReplyToStatus.URI, status.InReplyToURI) suite.Equal(inReplyToStatus.URI, status.InReplyToURI)
suite.True(*status.Federated) suite.True(*status.Federated)
suite.True(*status.Boostable)
suite.True(*status.Replyable)
suite.True(*status.Likeable)
suite.Equal(`<p><span class="h-card"><a href="http://localhost:8080/@the_mighty_zork" class="u-url mention">@<span>the_mighty_zork</span></a></span> nice there it is:</p><p><a href="http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity" rel="nofollow noopener noreferrer" target="_blank"><span class="invisible">https://</span><span class="ellipsis">social.pixie.town/users/f0x/st</span><span class="invisible">atuses/106221628567855262/activity</span></a></p>`, status.Content) suite.Equal(`<p><span class="h-card"><a href="http://localhost:8080/@the_mighty_zork" class="u-url mention">@<span>the_mighty_zork</span></a></span> nice there it is:</p><p><a href="http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity" rel="nofollow noopener noreferrer" target="_blank"><span class="invisible">https://</span><span class="ellipsis">social.pixie.town/users/f0x/st</span><span class="invisible">atuses/106221628567855262/activity</span></a></p>`, status.Content)
suite.Len(status.Mentions, 1) suite.Len(status.Mentions, 1)
m1 := status.Mentions[0] m1 := status.Mentions[0]

View file

@ -95,9 +95,6 @@ func (c *Converter) StatusToBoost(
BoostOfAccount: target.Account, BoostOfAccount: target.Account,
Visibility: target.Visibility, Visibility: target.Visibility,
Federated: util.Ptr(*target.Federated), Federated: util.Ptr(*target.Federated),
Boostable: util.Ptr(*target.Boostable),
Replyable: util.Ptr(*target.Replyable),
Likeable: util.Ptr(*target.Likeable),
} }
return boost, nil return boost, nil

View file

@ -98,9 +98,6 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() {
Visibility: gtsmodel.VisibilityDefault, Visibility: gtsmodel.VisibilityDefault,
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
} }
item, err := suite.typeconverter.StatusToRSSItem(context.Background(), s) item, err := suite.typeconverter.StatusToRSSItem(context.Background(), s)
suite.NoError(err) suite.NoError(err)

View file

@ -43,6 +43,7 @@ EXPECT=$(cat << "EOF"
"follow-request-mem-ratio": 2, "follow-request-mem-ratio": 2,
"in-reply-to-ids-mem-ratio": 3, "in-reply-to-ids-mem-ratio": 3,
"instance-mem-ratio": 1, "instance-mem-ratio": 1,
"interaction-approval-mem-ratio": 1,
"list-entry-mem-ratio": 2, "list-entry-mem-ratio": 2,
"list-mem-ratio": 1, "list-mem-ratio": 1,
"marker-mem-ratio": 0.5, "marker-mem-ratio": 0.5,

View file

@ -1401,9 +1401,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"admin_account_status_2": { "admin_account_status_2": {
@ -1427,9 +1424,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"admin_account_status_3": { "admin_account_status_3": {
@ -1454,9 +1448,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"admin_account_status_4": { "admin_account_status_4": {
@ -1482,9 +1473,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_1_status_1": { "local_account_1_status_1": {
@ -1507,9 +1495,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_1_status_2": { "local_account_1_status_2": {
@ -1532,9 +1517,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(false), Federated: util.Ptr(false),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_1_status_3": { "local_account_1_status_3": {
@ -1557,10 +1539,18 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(false), InteractionPolicy: &gtsmodel.InteractionPolicy{
Replyable: util.Ptr(false), CanLike: gtsmodel.PolicyRules{
Likeable: util.Ptr(false), Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
ActivityStreamsType: ap.ObjectNote, },
CanReply: gtsmodel.PolicyRules{
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
},
CanAnnounce: gtsmodel.PolicyRules{
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
},
},
ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_1_status_4": { "local_account_1_status_4": {
ID: "01F8MH82FYRXD2RC6108DAJ5HB", ID: "01F8MH82FYRXD2RC6108DAJ5HB",
@ -1583,9 +1573,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_1_status_5": { "local_account_1_status_5": {
@ -1609,9 +1596,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_1_status_6": { "local_account_1_status_6": {
@ -1635,9 +1619,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ActivityQuestion, ActivityStreamsType: ap.ActivityQuestion,
PollID: "01HEN2RKT1YTEZ80SA8HGP105F", PollID: "01HEN2RKT1YTEZ80SA8HGP105F",
}, },
@ -1661,9 +1642,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_2_status_1": { "local_account_2_status_1": {
@ -1686,9 +1664,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_2_status_2": { "local_account_2_status_2": {
@ -1711,17 +1686,25 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true), InteractionPolicy: &gtsmodel.InteractionPolicy{
Replyable: util.Ptr(false), CanLike: gtsmodel.PolicyRules{
Likeable: util.Ptr(true), Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
ActivityStreamsType: ap.ObjectNote, },
CanReply: gtsmodel.PolicyRules{
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
},
CanAnnounce: gtsmodel.PolicyRules{
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
},
},
ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_2_status_3": { "local_account_2_status_3": {
ID: "01F8MHC8VWDRBQR0N1BATDDEM5", ID: "01F8MHC8VWDRBQR0N1BATDDEM5",
URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5", URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
URL: "http://localhost:8080/@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🐢", 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 this one but I don't want likes or replies to it because cba🐢", 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"), CreatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"),
UpdatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"), UpdatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"),
Local: util.Ptr(true), Local: util.Ptr(true),
@ -1730,16 +1713,25 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
InReplyToID: "", InReplyToID: "",
BoostOfID: "", BoostOfID: "",
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0", 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, Visibility: gtsmodel.VisibilityUnlocked,
Sensitive: util.Ptr(true), Sensitive: util.Ptr(true),
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true), InteractionPolicy: &gtsmodel.InteractionPolicy{
Replyable: util.Ptr(false), CanLike: gtsmodel.PolicyRules{
Likeable: util.Ptr(false), Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
ActivityStreamsType: ap.ObjectNote, },
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": { "local_account_2_status_4": {
ID: "01F8MHCP5P2NWYQ416SBA0XSEV", ID: "01F8MHCP5P2NWYQ416SBA0XSEV",
@ -1761,10 +1753,17 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(false), Federated: util.Ptr(false),
Boostable: util.Ptr(false), InteractionPolicy: &gtsmodel.InteractionPolicy{
Replyable: util.Ptr(true), CanLike: gtsmodel.PolicyRules{
Likeable: util.Ptr(true), Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
},
CanReply: gtsmodel.PolicyRules{
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
},
CanAnnounce: gtsmodel.PolicyRules{
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
},
},
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_2_status_5": { "local_account_2_status_5": {
@ -1790,9 +1789,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_2_status_6": { "local_account_2_status_6": {
@ -1818,9 +1814,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_2_status_7": { "local_account_2_status_7": {
@ -1845,9 +1838,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"local_account_2_status_8": { "local_account_2_status_8": {
@ -1871,9 +1861,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ActivityQuestion, ActivityStreamsType: ap.ActivityQuestion,
PollID: "01HEN2QB5NR4NCEHGYC3HN84K6", PollID: "01HEN2QB5NR4NCEHGYC3HN84K6",
}, },
@ -1899,9 +1886,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
"remote_account_1_status_2": { "remote_account_1_status_2": {
@ -1925,9 +1909,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ActivityQuestion, ActivityStreamsType: ap.ActivityQuestion,
PollID: "01HEN2R65468ZG657C4ZPHJ4EX", PollID: "01HEN2R65468ZG657C4ZPHJ4EX",
}, },
@ -1952,9 +1933,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ActivityQuestion, ActivityStreamsType: ap.ActivityQuestion,
PollID: "01HEWV1GW2D49R919NPEDXPTZ5", PollID: "01HEWV1GW2D49R919NPEDXPTZ5",
}, },
@ -1980,9 +1958,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Language: "en", Language: "en",
CreatedWithApplicationID: "", CreatedWithApplicationID: "",
Federated: util.Ptr(true), Federated: util.Ptr(true),
Boostable: util.Ptr(true),
Replyable: util.Ptr(true),
Likeable: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote, ActivityStreamsType: ap.ObjectNote,
}, },
} }