mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-24 10:15:47 +03:00
[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:
parent
8f8093aea4
commit
5bc567196b
46 changed files with 1318 additions and 531 deletions
|
@ -8017,21 +8017,6 @@ paths:
|
|||
name: federated
|
||||
type: boolean
|
||||
x-go-name: Federated
|
||||
- description: This status can be boosted/reblogged.
|
||||
in: formData
|
||||
name: boostable
|
||||
type: boolean
|
||||
x-go-name: Boostable
|
||||
- description: This status can be replied to.
|
||||
in: formData
|
||||
name: replyable
|
||||
type: boolean
|
||||
x-go-name: Replyable
|
||||
- description: This status can be liked/faved.
|
||||
in: formData
|
||||
name: likeable
|
||||
type: boolean
|
||||
x-go-name: Likeable
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
|
@ -173,42 +173,43 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
|||
}
|
||||
|
||||
// try to boost a status that's not boostable / visible to us
|
||||
func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
||||
t := suite.testTokens["local_account_1"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
// TODO: sort this out with new interaction policies
|
||||
// func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
||||
// t := suite.testTokens["local_account_1"]
|
||||
// oauthToken := oauth.DBTokenToToken(t)
|
||||
|
||||
targetStatus := suite.testStatuses["local_account_2_status_4"]
|
||||
// targetStatus := suite.testStatuses["local_account_2_status_4"]
|
||||
|
||||
// setup
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
// // setup
|
||||
// recorder := httptest.NewRecorder()
|
||||
// ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
// ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
// ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
}
|
||||
// // normally the router would populate these params from the path values,
|
||||
// // but because we're calling the function directly, we need to set them manually.
|
||||
// ctx.Params = gin.Params{
|
||||
// gin.Param{
|
||||
// Key: statuses.IDKey,
|
||||
// Value: targetStatus.ID,
|
||||
// },
|
||||
// }
|
||||
|
||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||
// suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses
|
||||
// // check response
|
||||
// suite.Equal(http.StatusNotFound, recorder.Code) // we 404 unboostable statuses
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"error":"Not Found"}`, string(b))
|
||||
}
|
||||
// result := recorder.Result()
|
||||
// defer result.Body.Close()
|
||||
// b, err := ioutil.ReadAll(result.Body)
|
||||
// suite.NoError(err)
|
||||
// suite.Equal(`{"error":"Not Found"}`, string(b))
|
||||
// }
|
||||
|
||||
// try to boost a status that's not visible to the user
|
||||
func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
||||
|
|
|
@ -168,24 +168,6 @@ import (
|
|||
// description: This status will be federated beyond the local timeline(s).
|
||||
// in: formData
|
||||
// type: boolean
|
||||
// -
|
||||
// name: boostable
|
||||
// x-go-name: Boostable
|
||||
// description: This status can be boosted/reblogged.
|
||||
// in: formData
|
||||
// type: boolean
|
||||
// -
|
||||
// name: replyable
|
||||
// x-go-name: Replyable
|
||||
// description: This status can be replied to.
|
||||
// in: formData
|
||||
// type: boolean
|
||||
// -
|
||||
// name: likeable
|
||||
// x-go-name: Likeable
|
||||
// description: This status can be liked/faved.
|
||||
// in: formData
|
||||
// type: boolean
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
|
|
|
@ -67,9 +67,6 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
|||
"spoiler_text": {"hello hello"},
|
||||
"sensitive": {"true"},
|
||||
"visibility": {string(apimodel.VisibilityMutualsOnly)},
|
||||
"likeable": {"false"},
|
||||
"replyable": {"false"},
|
||||
"federated": {"false"},
|
||||
}
|
||||
suite.statusModule.StatusCreatePOSTHandler(ctx)
|
||||
|
||||
|
|
|
@ -89,42 +89,43 @@ func (suite *StatusFaveTestSuite) TestPostFave() {
|
|||
}
|
||||
|
||||
// try to fave a status that's not faveable
|
||||
func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||
t := suite.testTokens["local_account_1"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
// TODO: replace this when interaction policies enforced.
|
||||
// func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||
// t := suite.testTokens["local_account_1"]
|
||||
// oauthToken := oauth.DBTokenToToken(t)
|
||||
|
||||
targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable
|
||||
// targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable
|
||||
|
||||
// setup
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
// // setup
|
||||
// recorder := httptest.NewRecorder()
|
||||
// ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
// ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||
// ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
// ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
}
|
||||
// // normally the router would populate these params from the path values,
|
||||
// // but because we're calling the function directly, we need to set them manually.
|
||||
// ctx.Params = gin.Params{
|
||||
// gin.Param{
|
||||
// Key: statuses.IDKey,
|
||||
// Value: targetStatus.ID,
|
||||
// },
|
||||
// }
|
||||
|
||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||
// suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusForbidden, recorder.Code)
|
||||
// // check response
|
||||
// suite.EqualValues(http.StatusForbidden, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b))
|
||||
}
|
||||
// result := recorder.Result()
|
||||
// defer result.Body.Close()
|
||||
// b, err := ioutil.ReadAll(result.Body)
|
||||
// assert.NoError(suite.T(), err)
|
||||
// assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b))
|
||||
// }
|
||||
|
||||
func TestStatusFaveTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusFaveTestSuite))
|
||||
|
|
|
@ -183,9 +183,6 @@ func (suite *StatusPinTestSuite) TestPinStatusTooManyPins() {
|
|||
AccountURI: testAccount.URI,
|
||||
Visibility: gtsmodel.VisibilityPublic,
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, status); err != nil {
|
||||
|
|
|
@ -230,12 +230,6 @@ type AdvancedStatusCreateForm struct {
|
|||
type AdvancedVisibilityFlagsForm struct {
|
||||
// This status will be federated beyond the local timeline(s).
|
||||
Federated *bool `form:"federated" json:"federated" xml:"federated"`
|
||||
// This status can be boosted/reblogged.
|
||||
Boostable *bool `form:"boostable" json:"boostable" xml:"boostable"`
|
||||
// This status can be replied to.
|
||||
Replyable *bool `form:"replyable" json:"replyable" xml:"replyable"`
|
||||
// This status can be liked/faved.
|
||||
Likeable *bool `form:"likeable" json:"likeable" xml:"likeable"`
|
||||
}
|
||||
|
||||
// StatusContentType is the content type with which to parse the submitted status.
|
||||
|
|
2
internal/cache/cache.go
vendored
2
internal/cache/cache.go
vendored
|
@ -73,6 +73,7 @@ func (c *Caches) Init() {
|
|||
c.initFollowRequestIDs()
|
||||
c.initInReplyToIDs()
|
||||
c.initInstance()
|
||||
c.initInteractionApproval()
|
||||
c.initList()
|
||||
c.initListEntry()
|
||||
c.initMarker()
|
||||
|
@ -145,6 +146,7 @@ func (c *Caches) Sweep(threshold float64) {
|
|||
c.GTS.FollowRequestIDs.Trim(threshold)
|
||||
c.GTS.InReplyToIDs.Trim(threshold)
|
||||
c.GTS.Instance.Trim(threshold)
|
||||
c.GTS.InteractionApproval.Trim(threshold)
|
||||
c.GTS.List.Trim(threshold)
|
||||
c.GTS.ListEntry.Trim(threshold)
|
||||
c.GTS.Marker.Trim(threshold)
|
||||
|
|
37
internal/cache/db.go
vendored
37
internal/cache/db.go
vendored
|
@ -100,6 +100,9 @@ type GTSCaches struct {
|
|||
// Instance provides access to the gtsmodel Instance database cache.
|
||||
Instance StructCache[*gtsmodel.Instance]
|
||||
|
||||
// InteractionApproval provides access to the gtsmodel InteractionApproval database cache.
|
||||
InteractionApproval StructCache[*gtsmodel.InteractionApproval]
|
||||
|
||||
// InReplyToIDs provides access to the status in reply to IDs list database cache.
|
||||
InReplyToIDs SliceCache[string]
|
||||
|
||||
|
@ -737,6 +740,39 @@ func (c *Caches) initInstance() {
|
|||
})
|
||||
}
|
||||
|
||||
func (c *Caches) initInteractionApproval() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofInteractionApproval(),
|
||||
config.GetCacheInteractionApprovalMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "cache size = %d", cap)
|
||||
|
||||
copyF := func(i1 *gtsmodel.InteractionApproval) *gtsmodel.InteractionApproval {
|
||||
i2 := new(gtsmodel.InteractionApproval)
|
||||
*i2 = *i1
|
||||
|
||||
// Don't include ptr fields that
|
||||
// will be populated separately.
|
||||
// See internal/db/bundb/interaction.go.
|
||||
i2.Account = nil
|
||||
i2.InteractingAccount = nil
|
||||
|
||||
return i2
|
||||
}
|
||||
|
||||
c.GTS.InteractionApproval.Init(structr.CacheConfig[*gtsmodel.InteractionApproval]{
|
||||
Indices: []structr.IndexConfig{
|
||||
{Fields: "ID"},
|
||||
{Fields: "URI"},
|
||||
},
|
||||
MaxSize: cap,
|
||||
IgnoreErr: ignoreErrors,
|
||||
Copy: copyF,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Caches) initList() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
|
@ -1188,6 +1224,7 @@ func (c *Caches) initStatusFave() {
|
|||
c.GTS.StatusFave.Init(structr.CacheConfig[*gtsmodel.StatusFave]{
|
||||
Indices: []structr.IndexConfig{
|
||||
{Fields: "ID"},
|
||||
{Fields: "URI"},
|
||||
{Fields: "AccountID,StatusID"},
|
||||
{Fields: "StatusID", Multiple: true},
|
||||
},
|
||||
|
|
17
internal/cache/size.go
vendored
17
internal/cache/size.go
vendored
|
@ -189,6 +189,7 @@ func totalOfRatios() float64 {
|
|||
config.GetCacheFollowRequestMemRatio() +
|
||||
config.GetCacheFollowRequestIDsMemRatio() +
|
||||
config.GetCacheInstanceMemRatio() +
|
||||
config.GetCacheInteractionApprovalMemRatio() +
|
||||
config.GetCacheInReplyToIDsMemRatio() +
|
||||
config.GetCacheListMemRatio() +
|
||||
config.GetCacheListEntryMemRatio() +
|
||||
|
@ -425,6 +426,19 @@ func sizeofInstance() uintptr {
|
|||
}))
|
||||
}
|
||||
|
||||
func sizeofInteractionApproval() uintptr {
|
||||
return uintptr(size.Of(>smodel.InteractionApproval{
|
||||
ID: exampleID,
|
||||
CreatedAt: exampleTime,
|
||||
UpdatedAt: exampleTime,
|
||||
AccountID: exampleID,
|
||||
InteractingAccountID: exampleID,
|
||||
InteractionURI: exampleURI,
|
||||
InteractionType: gtsmodel.InteractionAnnounce,
|
||||
URI: exampleURI,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofList() uintptr {
|
||||
return uintptr(size.Of(>smodel.List{
|
||||
ID: exampleID,
|
||||
|
@ -591,9 +605,6 @@ func sizeofStatus() uintptr {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: exampleID,
|
||||
Federated: func() *bool { ok := true; return &ok }(),
|
||||
Boostable: func() *bool { ok := true; return &ok }(),
|
||||
Replyable: func() *bool { ok := true; return &ok }(),
|
||||
Likeable: func() *bool { ok := true; return &ok }(),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -212,6 +212,7 @@ type CacheConfiguration struct {
|
|||
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
|
||||
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
|
||||
InstanceMemRatio float64 `name:"instance-mem-ratio"`
|
||||
InteractionApprovalMemRatio float64 `name:"interaction-approval-mem-ratio"`
|
||||
ListMemRatio float64 `name:"list-mem-ratio"`
|
||||
ListEntryMemRatio float64 `name:"list-entry-mem-ratio"`
|
||||
MarkerMemRatio float64 `name:"marker-mem-ratio"`
|
||||
|
|
|
@ -176,6 +176,7 @@ var Defaults = Configuration{
|
|||
FollowRequestIDsMemRatio: 2,
|
||||
InReplyToIDsMemRatio: 3,
|
||||
InstanceMemRatio: 1,
|
||||
InteractionApprovalMemRatio: 1,
|
||||
ListMemRatio: 1,
|
||||
ListEntryMemRatio: 2,
|
||||
MarkerMemRatio: 0.5,
|
||||
|
|
|
@ -3250,6 +3250,33 @@ func GetCacheInstanceMemRatio() float64 { return global.GetCacheInstanceMemRatio
|
|||
// SetCacheInstanceMemRatio safely sets the value for global configuration 'Cache.InstanceMemRatio' field
|
||||
func SetCacheInstanceMemRatio(v float64) { global.SetCacheInstanceMemRatio(v) }
|
||||
|
||||
// GetCacheInteractionApprovalMemRatio safely fetches the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field
|
||||
func (st *ConfigState) GetCacheInteractionApprovalMemRatio() (v float64) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.Cache.InteractionApprovalMemRatio
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetCacheInteractionApprovalMemRatio safely sets the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field
|
||||
func (st *ConfigState) SetCacheInteractionApprovalMemRatio(v float64) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.Cache.InteractionApprovalMemRatio = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// CacheInteractionApprovalMemRatioFlag returns the flag name for the 'Cache.InteractionApprovalMemRatio' field
|
||||
func CacheInteractionApprovalMemRatioFlag() string { return "cache-interaction-approval-mem-ratio" }
|
||||
|
||||
// GetCacheInteractionApprovalMemRatio safely fetches the value for global configuration 'Cache.InteractionApprovalMemRatio' field
|
||||
func GetCacheInteractionApprovalMemRatio() float64 {
|
||||
return global.GetCacheInteractionApprovalMemRatio()
|
||||
}
|
||||
|
||||
// SetCacheInteractionApprovalMemRatio safely sets the value for global configuration 'Cache.InteractionApprovalMemRatio' field
|
||||
func SetCacheInteractionApprovalMemRatio(v float64) { global.SetCacheInteractionApprovalMemRatio(v) }
|
||||
|
||||
// GetCacheListMemRatio safely fetches the Configuration value for state's 'Cache.ListMemRatio' field
|
||||
func (st *ConfigState) GetCacheListMemRatio() (v float64) {
|
||||
st.mutex.RLock()
|
||||
|
|
|
@ -115,15 +115,6 @@ func (suite *AccountTestSuite) populateTestStatus(testAccountKey string, status
|
|||
if status.Federated == nil {
|
||||
status.Federated = util.Ptr(true)
|
||||
}
|
||||
if status.Boostable == nil {
|
||||
status.Boostable = util.Ptr(true)
|
||||
}
|
||||
if status.Likeable == nil {
|
||||
status.Likeable = util.Ptr(true)
|
||||
}
|
||||
if status.Replyable == nil {
|
||||
status.Replyable = util.Ptr(true)
|
||||
}
|
||||
|
||||
if inReplyTo != nil {
|
||||
status.InReplyToAccountID = inReplyTo.AccountID
|
||||
|
|
|
@ -60,6 +60,7 @@ type DBService struct {
|
|||
db.Emoji
|
||||
db.HeaderFilter
|
||||
db.Instance
|
||||
db.Interaction
|
||||
db.Filter
|
||||
db.List
|
||||
db.Marker
|
||||
|
@ -203,6 +204,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
db: db,
|
||||
state: state,
|
||||
},
|
||||
Interaction: &interactionDB{
|
||||
db: db,
|
||||
state: state,
|
||||
},
|
||||
Filter: &filterDB{
|
||||
db: db,
|
||||
state: state,
|
||||
|
|
149
internal/db/bundb/interaction.go
Normal file
149
internal/db/bundb/interaction.go
Normal 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
|
||||
}
|
|
@ -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 := >smodel.Status{
|
||||
ID: oldStatus.ID,
|
||||
InteractionPolicy: policy,
|
||||
}
|
||||
if _, err := tx.
|
||||
NewUpdate().
|
||||
Model(newStatus).
|
||||
Column("interaction_policy").
|
||||
Where("? = ?", bun.Ident("id"), newStatus.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Drop now unused columns from statuses table.
|
||||
oldColumns := []string{
|
||||
"likeable",
|
||||
"replyable",
|
||||
"boostable",
|
||||
}
|
||||
for _, column := range oldColumns {
|
||||
if _, err := tx.
|
||||
NewDropColumn().
|
||||
Table("statuses").
|
||||
Column(column).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add new indexes.
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("statuses").
|
||||
Index("statuses_pending_approval_idx").
|
||||
Column("pending_approval").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("status_faves").
|
||||
Index("status_faves_pending_approval_idx").
|
||||
Column("pending_approval").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -44,9 +44,6 @@ func (suite *StatusTestSuite) TestGetStatusByID() {
|
|||
suite.Nil(status.InReplyTo)
|
||||
suite.Nil(status.InReplyToAccount)
|
||||
suite.True(*status.Federated)
|
||||
suite.True(*status.Boostable)
|
||||
suite.True(*status.Replyable)
|
||||
suite.True(*status.Likeable)
|
||||
}
|
||||
|
||||
func (suite *StatusTestSuite) TestGetStatusesByIDs() {
|
||||
|
@ -73,9 +70,6 @@ func (suite *StatusTestSuite) TestGetStatusesByIDs() {
|
|||
suite.Nil(status1.InReplyTo)
|
||||
suite.Nil(status1.InReplyToAccount)
|
||||
suite.True(*status1.Federated)
|
||||
suite.True(*status1.Boostable)
|
||||
suite.True(*status1.Replyable)
|
||||
suite.True(*status1.Likeable)
|
||||
|
||||
status2 := statuses[1]
|
||||
suite.NotNil(status2)
|
||||
|
@ -86,9 +80,6 @@ func (suite *StatusTestSuite) TestGetStatusesByIDs() {
|
|||
suite.Nil(status2.InReplyTo)
|
||||
suite.Nil(status2.InReplyToAccount)
|
||||
suite.True(*status2.Federated)
|
||||
suite.True(*status2.Boostable)
|
||||
suite.False(*status2.Replyable)
|
||||
suite.False(*status2.Likeable)
|
||||
}
|
||||
|
||||
func (suite *StatusTestSuite) TestGetStatusByURI() {
|
||||
|
@ -104,9 +95,6 @@ func (suite *StatusTestSuite) TestGetStatusByURI() {
|
|||
suite.Nil(status.InReplyTo)
|
||||
suite.Nil(status.InReplyToAccount)
|
||||
suite.True(*status.Federated)
|
||||
suite.True(*status.Boostable)
|
||||
suite.False(*status.Replyable)
|
||||
suite.False(*status.Likeable)
|
||||
}
|
||||
|
||||
func (suite *StatusTestSuite) TestGetStatusWithExtras() {
|
||||
|
@ -121,9 +109,6 @@ func (suite *StatusTestSuite) TestGetStatusWithExtras() {
|
|||
suite.NotEmpty(status.Attachments)
|
||||
suite.NotEmpty(status.Emojis)
|
||||
suite.True(*status.Federated)
|
||||
suite.True(*status.Boostable)
|
||||
suite.True(*status.Replyable)
|
||||
suite.True(*status.Likeable)
|
||||
}
|
||||
|
||||
func (suite *StatusTestSuite) TestGetStatusWithMention() {
|
||||
|
@ -138,9 +123,6 @@ func (suite *StatusTestSuite) TestGetStatusWithMention() {
|
|||
suite.NotEmpty(status.InReplyToID)
|
||||
suite.NotEmpty(status.InReplyToAccountID)
|
||||
suite.True(*status.Federated)
|
||||
suite.True(*status.Boostable)
|
||||
suite.True(*status.Replyable)
|
||||
suite.True(*status.Likeable)
|
||||
}
|
||||
|
||||
// The below test was originally used to ensure that a second
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
|
@ -77,6 +78,21 @@ func (s *statusFaveDB) GetStatusFaveByID(ctx context.Context, id string) (*gtsmo
|
|||
)
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) GetStatusFaveByURI(ctx context.Context, uri string) (*gtsmodel.StatusFave, error) {
|
||||
return s.getStatusFave(
|
||||
ctx,
|
||||
"URI",
|
||||
func(fave *gtsmodel.StatusFave) error {
|
||||
return s.db.
|
||||
NewSelect().
|
||||
Model(fave).
|
||||
Where("? = ?", bun.Ident("uri"), uri).
|
||||
Scan(ctx)
|
||||
},
|
||||
uri,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) getStatusFave(ctx context.Context, lookup string, dbQuery func(*gtsmodel.StatusFave) error, keyParts ...any) (*gtsmodel.StatusFave, error) {
|
||||
// Fetch status fave from database cache with loader callback
|
||||
fave, err := s.state.Caches.GTS.StatusFave.LoadOne(lookup, func() (*gtsmodel.StatusFave, error) {
|
||||
|
@ -242,6 +258,26 @@ func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusF
|
|||
})
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) UpdateStatusFave(ctx context.Context, fave *gtsmodel.StatusFave, columns ...string) error {
|
||||
fave.UpdatedAt = time.Now()
|
||||
if len(columns) > 0 {
|
||||
// If we're updating by column,
|
||||
// ensure "updated_at" is included.
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
// Update the status fave model in the database.
|
||||
return s.state.Caches.GTS.StatusFave.Store(fave, func() error {
|
||||
_, err := s.db.
|
||||
NewUpdate().
|
||||
Model(fave).
|
||||
Where("? = ?", bun.Ident("status_fave.id"), fave.ID).
|
||||
Column(columns...).
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) DeleteStatusFaveByID(ctx context.Context, id string) error {
|
||||
var statusID string
|
||||
|
||||
|
|
|
@ -65,9 +65,7 @@ func getFutureStatus() *gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
InteractionPolicy: gtsmodel.DefaultInteractionPolicyPublic(),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ type DB interface {
|
|||
Emoji
|
||||
HeaderFilter
|
||||
Instance
|
||||
Interaction
|
||||
Filter
|
||||
List
|
||||
Marker
|
||||
|
|
41
internal/db/interaction.go
Normal file
41
internal/db/interaction.go
Normal 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
|
||||
}
|
|
@ -27,9 +27,12 @@ type StatusFave interface {
|
|||
// GetStatusFaveByAccountID gets one status fave created by the given accountID, targeting the given statusID.
|
||||
GetStatusFave(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, error)
|
||||
|
||||
// GetStatusFave returns one status fave with the given id.
|
||||
// GetStatusFaveByID returns one status fave with the given id.
|
||||
GetStatusFaveByID(ctx context.Context, id string) (*gtsmodel.StatusFave, error)
|
||||
|
||||
// GetStatusFaveByURI returns one status fave with the given uri.
|
||||
GetStatusFaveByURI(ctx context.Context, uri string) (*gtsmodel.StatusFave, error)
|
||||
|
||||
// GetStatusFaves returns a slice of faves/likes of the status with given ID.
|
||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||
GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, error)
|
||||
|
@ -40,6 +43,9 @@ type StatusFave interface {
|
|||
// PutStatusFave inserts the given statusFave into the database.
|
||||
PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error
|
||||
|
||||
// UpdateStatusFave updates one statusFave in the database.
|
||||
UpdateStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave, columns ...string) error
|
||||
|
||||
// DeleteStatusFave deletes one status fave with the given id.
|
||||
DeleteStatusFaveByID(ctx context.Context, id string) error
|
||||
|
||||
|
|
|
@ -92,9 +92,6 @@ func (d *Dereferencer) EnrichAnnounce(
|
|||
boost.BoostOfAccount = target.Account
|
||||
boost.Visibility = target.Visibility
|
||||
boost.Federated = target.Federated
|
||||
boost.Boostable = target.Boostable
|
||||
boost.Replyable = target.Replyable
|
||||
boost.Likeable = target.Likeable
|
||||
|
||||
// Store the boost wrapper status in database.
|
||||
switch err = d.state.DB.PutStatus(ctx, boost); {
|
||||
|
|
|
@ -633,9 +633,7 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
}
|
||||
}
|
||||
|
||||
if permitted &&
|
||||
*status.InReplyTo.Replyable {
|
||||
// Status is reply-able to.
|
||||
if permitted {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -56,9 +56,6 @@ func (suite *StatusTestSuite) TestDereferenceSimpleStatus() {
|
|||
suite.NoError(err)
|
||||
suite.Equal(status.ID, dbStatus.ID)
|
||||
suite.True(*dbStatus.Federated)
|
||||
suite.True(*dbStatus.Boostable)
|
||||
suite.True(*dbStatus.Replyable)
|
||||
suite.True(*dbStatus.Likeable)
|
||||
|
||||
// account should be in the database now too
|
||||
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)
|
||||
|
@ -96,9 +93,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithMention() {
|
|||
suite.NoError(err)
|
||||
suite.Equal(status.ID, dbStatus.ID)
|
||||
suite.True(*dbStatus.Federated)
|
||||
suite.True(*dbStatus.Boostable)
|
||||
suite.True(*dbStatus.Replyable)
|
||||
suite.True(*dbStatus.Likeable)
|
||||
|
||||
// account should be in the database now too
|
||||
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)
|
||||
|
@ -151,9 +145,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithTag() {
|
|||
suite.NoError(err)
|
||||
suite.Equal(status.ID, dbStatus.ID)
|
||||
suite.True(*dbStatus.Federated)
|
||||
suite.True(*dbStatus.Boostable)
|
||||
suite.True(*dbStatus.Replyable)
|
||||
suite.True(*dbStatus.Likeable)
|
||||
|
||||
// account should be in the database now too
|
||||
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)
|
||||
|
@ -197,9 +188,6 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithImageAndNoContent() {
|
|||
suite.NoError(err)
|
||||
suite.Equal(status.ID, dbStatus.ID)
|
||||
suite.True(*dbStatus.Federated)
|
||||
suite.True(*dbStatus.Boostable)
|
||||
suite.True(*dbStatus.Replyable)
|
||||
suite.True(*dbStatus.Likeable)
|
||||
|
||||
// account should be in the database now too
|
||||
account, err := suite.db.GetAccountByURI(context.Background(), status.AccountURI)
|
||||
|
|
|
@ -53,10 +53,5 @@ func (f *Filter) StatusBoostable(ctx context.Context, requester *gtsmodel.Accoun
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if !*status.Boostable {
|
||||
log.Trace(ctx, "status marked not boostable")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -161,9 +161,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestThread() {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
|
||||
|
@ -214,9 +211,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, originalStatus); err != nil {
|
||||
|
@ -248,9 +242,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
|
||||
|
@ -282,9 +273,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyFollowersOnly(
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil {
|
||||
|
@ -327,9 +315,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, originalStatus); err != nil {
|
||||
|
@ -361,9 +346,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, firstReplyStatus); err != nil {
|
||||
|
@ -395,9 +377,6 @@ func (suite *StatusStatusHomeTimelineableTestSuite) TestChainReplyPublicAndUnloc
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}
|
||||
if err := suite.db.PutStatus(ctx, secondReplyStatus); err != nil {
|
||||
|
|
|
@ -32,4 +32,9 @@ type AccountSettings struct {
|
|||
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
|
||||
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
|
||||
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
|
||||
InteractionPolicyDirect *InteractionPolicy `bun:""` // Interaction policy to use for new direct visibility statuses by this account. If null, assume default policy.
|
||||
InteractionPolicyMutualsOnly *InteractionPolicy `bun:""` // Interaction policy to use for new mutuals only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyFollowersOnly *InteractionPolicy `bun:""` // Interaction policy to use for new followers only visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyUnlocked *InteractionPolicy `bun:""` // Interaction policy to use for new unlocked visibility statuses. If null, assume default policy.
|
||||
InteractionPolicyPublic *InteractionPolicy `bun:""` // Interaction policy to use for new public visibility statuses. If null, assume default policy.
|
||||
}
|
||||
|
|
55
internal/gtsmodel/interactionapproval.go
Normal file
55
internal/gtsmodel/interactionapproval.go
Normal 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
|
||||
)
|
314
internal/gtsmodel/interactionpolicy.go
Normal file
314
internal/gtsmodel/interactionpolicy.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -47,4 +47,7 @@ const (
|
|||
NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended
|
||||
NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status.
|
||||
NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance.
|
||||
NotificationPendingFave NotificationType = "pending.favourite" // Someone has faved a status of yours, which requires approval by you.
|
||||
NotificationPendingReply NotificationType = "pending.reply" // Someone has replied to a status of yours, which requires approval by you.
|
||||
NotificationPendingReblog NotificationType = "pending.reblog" // Someone has boosted a status of yours, which requires approval by you.
|
||||
)
|
||||
|
|
|
@ -66,9 +66,9 @@ type Status struct {
|
|||
ActivityStreamsType string `bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!.
|
||||
Text string `bun:""` // Original text of the status without formatting
|
||||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
Boostable *bool `bun:",notnull"` // This status can be boosted/reblogged
|
||||
Replyable *bool `bun:",notnull"` // This status can be replied to
|
||||
Likeable *bool `bun:",notnull"` // This status can be liked/faved
|
||||
InteractionPolicy *InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers.
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to.
|
||||
}
|
||||
|
||||
// GetID implements timeline.Timelineable{}.
|
||||
|
|
|
@ -31,4 +31,6 @@ type StatusFave struct {
|
|||
StatusID string `bun:"type:CHAR(26),unique:statusfaveaccountstatus,nullzero,notnull"` // database id of the status that has been 'faved'
|
||||
Status *Status `bun:"-"` // the faved status
|
||||
URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI of this fave
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then Like must be Approved by the like-ee before being fully distributed.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves this Like.
|
||||
}
|
||||
|
|
|
@ -185,11 +185,6 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac
|
|||
return errWithCode
|
||||
}
|
||||
|
||||
if !*inReplyTo.Replyable {
|
||||
const text = "in-reply-to status marked as not replyable"
|
||||
return gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Set status fields from inReplyTo.
|
||||
status.InReplyToID = inReplyTo.ID
|
||||
status.InReplyTo = inReplyTo
|
||||
|
@ -289,9 +284,6 @@ func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.Advanced
|
|||
func processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
|
||||
// by default all flags are set to true
|
||||
federated := true
|
||||
boostable := true
|
||||
replyable := true
|
||||
likeable := true
|
||||
|
||||
// If visibility isn't set on the form, then just take the account default.
|
||||
// If that's also not set, take the default for the whole instance.
|
||||
|
@ -305,57 +297,10 @@ func processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVi
|
|||
vis = gtsmodel.VisibilityDefault
|
||||
}
|
||||
|
||||
switch vis {
|
||||
case gtsmodel.VisibilityPublic:
|
||||
// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
|
||||
break
|
||||
case gtsmodel.VisibilityUnlocked:
|
||||
// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
|
||||
if form.Federated != nil {
|
||||
federated = *form.Federated
|
||||
}
|
||||
|
||||
if form.Boostable != nil {
|
||||
boostable = *form.Boostable
|
||||
}
|
||||
|
||||
if form.Replyable != nil {
|
||||
replyable = *form.Replyable
|
||||
}
|
||||
|
||||
if form.Likeable != nil {
|
||||
likeable = *form.Likeable
|
||||
}
|
||||
|
||||
case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
|
||||
// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
|
||||
boostable = false
|
||||
|
||||
if form.Federated != nil {
|
||||
federated = *form.Federated
|
||||
}
|
||||
|
||||
if form.Replyable != nil {
|
||||
replyable = *form.Replyable
|
||||
}
|
||||
|
||||
if form.Likeable != nil {
|
||||
likeable = *form.Likeable
|
||||
}
|
||||
|
||||
case gtsmodel.VisibilityDirect:
|
||||
// direct is pretty easy: there's only one possible setting so return it
|
||||
federated = true
|
||||
boostable = false
|
||||
replyable = true
|
||||
likeable = true
|
||||
}
|
||||
// Todo: sort out likeable/replyable/boostable in next PR.
|
||||
|
||||
status.Visibility = vis
|
||||
status.Federated = &federated
|
||||
status.Boostable = &boostable
|
||||
status.Replyable = &replyable
|
||||
status.Likeable = &likeable
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -53,9 +53,6 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks(
|
|||
},
|
||||
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
|
||||
Federated: nil,
|
||||
Boostable: nil,
|
||||
Replyable: nil,
|
||||
Likeable: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -87,9 +84,6 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuot
|
|||
},
|
||||
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
|
||||
Federated: nil,
|
||||
Boostable: nil,
|
||||
Replyable: nil,
|
||||
Likeable: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -125,9 +119,6 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji
|
|||
},
|
||||
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
|
||||
Federated: nil,
|
||||
Boostable: nil,
|
||||
Replyable: nil,
|
||||
Likeable: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -159,9 +150,6 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj
|
|||
},
|
||||
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
|
||||
Federated: nil,
|
||||
Boostable: nil,
|
||||
Replyable: nil,
|
||||
Likeable: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -197,9 +185,6 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
|
|||
},
|
||||
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
|
||||
Federated: nil,
|
||||
Boostable: nil,
|
||||
Replyable: nil,
|
||||
Likeable: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -229,9 +214,6 @@ func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() {
|
|||
},
|
||||
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
|
||||
Federated: nil,
|
||||
Boostable: nil,
|
||||
Replyable: nil,
|
||||
Likeable: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -266,9 +248,6 @@ func (suite *StatusCreateTestSuite) TestProcessReplyToUnthreadedRemoteStatus() {
|
|||
},
|
||||
AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
|
||||
Federated: nil,
|
||||
Boostable: nil,
|
||||
Replyable: nil,
|
||||
Likeable: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -62,11 +62,6 @@ func (p *Processor) getFaveableStatus(
|
|||
return nil, nil, errWithCode
|
||||
}
|
||||
|
||||
if !*target.Likeable {
|
||||
err := errors.New("status is not faveable")
|
||||
return nil, nil, gtserror.NewErrorForbidden(err, err.Error())
|
||||
}
|
||||
|
||||
fave, err := p.state.DB.GetStatusFave(ctx, requester.ID, target.ID)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
|
||||
|
|
|
@ -69,9 +69,6 @@ func (suite *FromClientAPITestSuite) newStatus(
|
|||
Visibility: visibility,
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
}
|
||||
|
||||
if replyToStatus != nil {
|
||||
|
|
|
@ -89,77 +89,78 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
|||
suite.False(*notif.Read)
|
||||
}
|
||||
|
||||
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||
testStructs := suite.SetupTestStructs()
|
||||
defer suite.TearDownTestStructs(testStructs)
|
||||
// Todo: fix this test up in interaction policies PR.
|
||||
// func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||
// testStructs := suite.SetupTestStructs()
|
||||
// defer suite.TearDownTestStructs(testStructs)
|
||||
|
||||
repliedAccount := suite.testAccounts["local_account_1"]
|
||||
repliedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
replyingAccount := suite.testAccounts["remote_account_1"]
|
||||
// repliedAccount := suite.testAccounts["local_account_1"]
|
||||
// repliedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
// replyingAccount := suite.testAccounts["remote_account_1"]
|
||||
|
||||
// Set the replyingAccount's last fetched_at
|
||||
// date to something recent so no refresh is attempted,
|
||||
// and ensure it isn't a suspended account.
|
||||
replyingAccount.FetchedAt = time.Now()
|
||||
replyingAccount.SuspendedAt = time.Time{}
|
||||
replyingAccount.SuspensionOrigin = ""
|
||||
err := testStructs.State.DB.UpdateAccount(context.Background(),
|
||||
replyingAccount,
|
||||
"fetched_at",
|
||||
"suspended_at",
|
||||
"suspension_origin",
|
||||
)
|
||||
suite.NoError(err)
|
||||
// // Set the replyingAccount's last fetched_at
|
||||
// // date to something recent so no refresh is attempted,
|
||||
// // and ensure it isn't a suspended account.
|
||||
// replyingAccount.FetchedAt = time.Now()
|
||||
// replyingAccount.SuspendedAt = time.Time{}
|
||||
// replyingAccount.SuspensionOrigin = ""
|
||||
// err := testStructs.State.DB.UpdateAccount(context.Background(),
|
||||
// replyingAccount,
|
||||
// "fetched_at",
|
||||
// "suspended_at",
|
||||
// "suspension_origin",
|
||||
// )
|
||||
// suite.NoError(err)
|
||||
|
||||
// Get replying statusable to use from remote test statuses.
|
||||
const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552"
|
||||
replyingStatusable := testrig.NewTestFediStatuses()[replyingURI]
|
||||
ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI))
|
||||
// // Get replying statusable to use from remote test statuses.
|
||||
// const replyingURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552"
|
||||
// replyingStatusable := testrig.NewTestFediStatuses()[replyingURI]
|
||||
// ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI))
|
||||
|
||||
// Open a websocket stream to later test the streamed status reply.
|
||||
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
|
||||
suite.NoError(errWithCode)
|
||||
// // Open a websocket stream to later test the streamed status reply.
|
||||
// wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
|
||||
// suite.NoError(errWithCode)
|
||||
|
||||
// Send the replied status off to the fedi worker to be further processed.
|
||||
err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
APObject: replyingStatusable,
|
||||
Receiving: repliedAccount,
|
||||
Requesting: replyingAccount,
|
||||
})
|
||||
suite.NoError(err)
|
||||
// // Send the replied status off to the fedi worker to be further processed.
|
||||
// err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
|
||||
// APObjectType: ap.ObjectNote,
|
||||
// APActivityType: ap.ActivityCreate,
|
||||
// APObject: replyingStatusable,
|
||||
// Receiving: repliedAccount,
|
||||
// Requesting: replyingAccount,
|
||||
// })
|
||||
// suite.NoError(err)
|
||||
|
||||
// side effects should be triggered
|
||||
// 1. status should be in the database
|
||||
replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI)
|
||||
suite.NoError(err)
|
||||
// // side effects should be triggered
|
||||
// // 1. status should be in the database
|
||||
// replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI)
|
||||
// suite.NoError(err)
|
||||
|
||||
// 2. a notification should exist for the mention
|
||||
var notif gtsmodel.Notification
|
||||
err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{
|
||||
{Key: "status_id", Value: replyingStatus.ID},
|
||||
}, ¬if)
|
||||
suite.NoError(err)
|
||||
suite.Equal(gtsmodel.NotificationMention, notif.NotificationType)
|
||||
suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID)
|
||||
suite.Equal(replyingStatus.AccountID, notif.OriginAccountID)
|
||||
suite.Equal(replyingStatus.ID, notif.StatusID)
|
||||
suite.False(*notif.Read)
|
||||
// // 2. a notification should exist for the mention
|
||||
// var notif gtsmodel.Notification
|
||||
// err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{
|
||||
// {Key: "status_id", Value: replyingStatus.ID},
|
||||
// }, ¬if)
|
||||
// suite.NoError(err)
|
||||
// suite.Equal(gtsmodel.NotificationMention, notif.NotificationType)
|
||||
// suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID)
|
||||
// suite.Equal(replyingStatus.AccountID, notif.OriginAccountID)
|
||||
// suite.Equal(replyingStatus.ID, notif.StatusID)
|
||||
// suite.False(*notif.Read)
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
|
||||
msg, ok := wssStream.Recv(ctx)
|
||||
suite.True(ok)
|
||||
// ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
|
||||
// msg, ok := wssStream.Recv(ctx)
|
||||
// suite.True(ok)
|
||||
|
||||
suite.Equal(stream.EventTypeNotification, msg.Event)
|
||||
suite.NotEmpty(msg.Payload)
|
||||
suite.EqualValues([]string{stream.TimelineHome}, msg.Stream)
|
||||
notifStreamed := &apimodel.Notification{}
|
||||
err = json.Unmarshal([]byte(msg.Payload), notifStreamed)
|
||||
suite.NoError(err)
|
||||
suite.Equal("mention", notifStreamed.Type)
|
||||
suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
|
||||
}
|
||||
// suite.Equal(stream.EventTypeNotification, msg.Event)
|
||||
// suite.NotEmpty(msg.Payload)
|
||||
// suite.EqualValues([]string{stream.TimelineHome}, msg.Stream)
|
||||
// notifStreamed := &apimodel.Notification{}
|
||||
// err = json.Unmarshal([]byte(msg.Payload), notifStreamed)
|
||||
// suite.NoError(err)
|
||||
// suite.Equal("mention", notifStreamed.Type)
|
||||
// suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
|
||||
// }
|
||||
|
||||
func (suite *FromFediAPITestSuite) TestProcessFave() {
|
||||
testStructs := suite.SetupTestStructs()
|
||||
|
|
|
@ -399,9 +399,6 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
|
|||
// needs to be created for this in go-fed/activity.
|
||||
// Until this is implemented, assume all true.
|
||||
status.Federated = util.Ptr(true)
|
||||
status.Boostable = util.Ptr(true)
|
||||
status.Replyable = util.Ptr(true)
|
||||
status.Likeable = util.Ptr(true)
|
||||
|
||||
// status.Sensitive
|
||||
sensitive := ap.ExtractSensitive(statusable)
|
||||
|
|
|
@ -181,9 +181,6 @@ func (suite *ASToInternalTestSuite) TestParseReplyWithMention() {
|
|||
suite.Equal(inReplyToStatus.ID, status.InReplyToID)
|
||||
suite.Equal(inReplyToStatus.URI, status.InReplyToURI)
|
||||
suite.True(*status.Federated)
|
||||
suite.True(*status.Boostable)
|
||||
suite.True(*status.Replyable)
|
||||
suite.True(*status.Likeable)
|
||||
suite.Equal(`<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)
|
||||
m1 := status.Mentions[0]
|
||||
|
|
|
@ -95,9 +95,6 @@ func (c *Converter) StatusToBoost(
|
|||
BoostOfAccount: target.Account,
|
||||
Visibility: target.Visibility,
|
||||
Federated: util.Ptr(*target.Federated),
|
||||
Boostable: util.Ptr(*target.Boostable),
|
||||
Replyable: util.Ptr(*target.Replyable),
|
||||
Likeable: util.Ptr(*target.Likeable),
|
||||
}
|
||||
|
||||
return boost, nil
|
||||
|
|
|
@ -98,9 +98,6 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() {
|
|||
Visibility: gtsmodel.VisibilityDefault,
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
}
|
||||
item, err := suite.typeconverter.StatusToRSSItem(context.Background(), s)
|
||||
suite.NoError(err)
|
||||
|
|
|
@ -43,6 +43,7 @@ EXPECT=$(cat << "EOF"
|
|||
"follow-request-mem-ratio": 2,
|
||||
"in-reply-to-ids-mem-ratio": 3,
|
||||
"instance-mem-ratio": 1,
|
||||
"interaction-approval-mem-ratio": 1,
|
||||
"list-entry-mem-ratio": 2,
|
||||
"list-mem-ratio": 1,
|
||||
"marker-mem-ratio": 0.5,
|
||||
|
|
|
@ -1401,9 +1401,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"admin_account_status_2": {
|
||||
|
@ -1427,9 +1424,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"admin_account_status_3": {
|
||||
|
@ -1454,9 +1448,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"admin_account_status_4": {
|
||||
|
@ -1482,9 +1473,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_1_status_1": {
|
||||
|
@ -1507,9 +1495,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_1_status_2": {
|
||||
|
@ -1532,9 +1517,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(false),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_1_status_3": {
|
||||
|
@ -1557,9 +1539,17 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(false),
|
||||
Replyable: util.Ptr(false),
|
||||
Likeable: util.Ptr(false),
|
||||
InteractionPolicy: >smodel.InteractionPolicy{
|
||||
CanLike: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
|
||||
},
|
||||
CanReply: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
|
||||
},
|
||||
CanAnnounce: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
|
||||
},
|
||||
},
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_1_status_4": {
|
||||
|
@ -1583,9 +1573,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_1_status_5": {
|
||||
|
@ -1609,9 +1596,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_1_status_6": {
|
||||
|
@ -1635,9 +1619,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ActivityQuestion,
|
||||
PollID: "01HEN2RKT1YTEZ80SA8HGP105F",
|
||||
},
|
||||
|
@ -1661,9 +1642,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_1": {
|
||||
|
@ -1686,9 +1664,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_2": {
|
||||
|
@ -1711,17 +1686,25 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(false),
|
||||
Likeable: util.Ptr(true),
|
||||
InteractionPolicy: >smodel.InteractionPolicy{
|
||||
CanLike: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
|
||||
},
|
||||
CanReply: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
|
||||
},
|
||||
CanAnnounce: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
|
||||
},
|
||||
},
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_3": {
|
||||
ID: "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
URL: "http://localhost:8080/@1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
Content: "🐢 i don't mind people sharing this one but I don't want likes or replies to it because cba🐢",
|
||||
Text: "🐢 i don't mind people sharing this one but I don't want likes or replies to it because cba🐢",
|
||||
Content: "🐢 i don't mind people sharing and liking this one but I want to moderate replies to it 🐢",
|
||||
Text: "🐢 i don't mind people sharing and liking this one but I want to moderate replies to it 🐢",
|
||||
CreatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"),
|
||||
UpdatedAt: TimeMustParse("2021-10-20T12:40:37+02:00"),
|
||||
Local: util.Ptr(true),
|
||||
|
@ -1730,15 +1713,24 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
InReplyToID: "",
|
||||
BoostOfID: "",
|
||||
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||
ContentWarning: "you won't be able to like or reply to this",
|
||||
ContentWarning: "you won't be able to reply to this without my approval",
|
||||
Visibility: gtsmodel.VisibilityUnlocked,
|
||||
Sensitive: util.Ptr(true),
|
||||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(false),
|
||||
Likeable: util.Ptr(false),
|
||||
InteractionPolicy: >smodel.InteractionPolicy{
|
||||
CanLike: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
|
||||
},
|
||||
CanReply: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
|
||||
WithApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
|
||||
},
|
||||
CanAnnounce: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
|
||||
},
|
||||
},
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_4": {
|
||||
|
@ -1761,10 +1753,17 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(false),
|
||||
Boostable: util.Ptr(false),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
|
||||
InteractionPolicy: >smodel.InteractionPolicy{
|
||||
CanLike: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
|
||||
},
|
||||
CanReply: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic},
|
||||
},
|
||||
CanAnnounce: gtsmodel.PolicyRules{
|
||||
Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor},
|
||||
},
|
||||
},
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_5": {
|
||||
|
@ -1790,9 +1789,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_6": {
|
||||
|
@ -1818,9 +1814,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_7": {
|
||||
|
@ -1845,9 +1838,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_8": {
|
||||
|
@ -1871,9 +1861,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ActivityQuestion,
|
||||
PollID: "01HEN2QB5NR4NCEHGYC3HN84K6",
|
||||
},
|
||||
|
@ -1899,9 +1886,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"remote_account_1_status_2": {
|
||||
|
@ -1925,9 +1909,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ActivityQuestion,
|
||||
PollID: "01HEN2R65468ZG657C4ZPHJ4EX",
|
||||
},
|
||||
|
@ -1952,9 +1933,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ActivityQuestion,
|
||||
PollID: "01HEWV1GW2D49R919NPEDXPTZ5",
|
||||
},
|
||||
|
@ -1980,9 +1958,6 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Language: "en",
|
||||
CreatedWithApplicationID: "",
|
||||
Federated: util.Ptr(true),
|
||||
Boostable: util.Ptr(true),
|
||||
Replyable: util.Ptr(true),
|
||||
Likeable: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue