Skip to content

Commit

Permalink
[feature] Use local_only field, deprecate federated field (#3222)
Browse files Browse the repository at this point in the history
* [feature] Use `local_only` field, deprecate `federated` field

* use `deprecated` comment for form.Federated

* nolint
  • Loading branch information
tsmethurst authored Aug 22, 2024
1 parent ffcf6e7 commit 53fccb8
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 171 deletions.
24 changes: 19 additions & 5 deletions docs/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2635,6 +2635,10 @@ definitions:
example: en
type: string
x-go-name: Language
local_only:
description: Set to "true" if status is not federated, ie., a "local only" status; omitted from response otherwise.
type: boolean
x-go-name: LocalOnly
media_attachments:
description: Media that is attached to this status.
items:
Expand Down Expand Up @@ -2828,6 +2832,10 @@ definitions:
example: en
type: string
x-go-name: Language
local_only:
description: Set to "true" if status is not federated, ie., a "local only" status; omitted from response otherwise.
type: boolean
x-go-name: LocalOnly
media_attachments:
description: Media that is attached to this status.
items:
Expand Down Expand Up @@ -8654,6 +8662,17 @@ paths:
name: visibility
type: string
x-go-name: Visibility
- default: false
description: If set to true, this status will be "local only" and will NOT be federated beyond the local timeline(s). If set to false (default), this status will be federated to your followers beyond the local timeline(s).
in: formData
name: local_only
type: boolean
x-go-name: LocalOnly
- description: '***DEPRECATED***. Included for back compat only. Only used if set and local_only is not yet. If set to true, this status will be federated beyond the local timeline(s). If set to false, this status will NOT be federated beyond the local timeline(s).'
in: formData
name: federated
type: boolean
x-go-name: Federated
- description: |-
ISO 8601 Datetime at which to schedule a status.
Providing this parameter will cause ScheduledStatus to be returned instead of Status.
Expand All @@ -8677,11 +8696,6 @@ paths:
name: content_type
type: string
x-go-name: ContentType
- description: This status will be federated beyond the local timeline(s).
in: formData
name: federated
type: boolean
x-go-name: Federated
produces:
- application/json
responses:
Expand Down
37 changes: 28 additions & 9 deletions internal/api/client/statuses/statuscreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/internal/validate"
)

Expand Down Expand Up @@ -137,6 +138,24 @@ import (
// - direct
// in: formData
// -
// name: local_only
// x-go-name: LocalOnly
// description: >-
// If set to true, this status will be "local only" and will NOT be federated beyond the local timeline(s).
// If set to false (default), this status will be federated to your followers beyond the local timeline(s).
// type: boolean
// in: formData
// default: false
// -
// name: federated
// x-go-name: Federated
// description: >-
// ***DEPRECATED***. Included for back compat only. Only used if set and local_only is not yet.
// If set to true, this status will be federated beyond the local timeline(s).
// If set to false, this status will NOT be federated beyond the local timeline(s).
// in: formData
// type: boolean
// -
// name: scheduled_at
// x-go-name: ScheduledAt
// description: |-
Expand All @@ -162,12 +181,6 @@ import (
// - text/plain
// - text/markdown
// in: formData
// -
// name: federated
// x-go-name: Federated
// description: This status will be federated beyond the local timeline(s).
// in: formData
// type: boolean
//
// produces:
// - application/json
Expand Down Expand Up @@ -210,7 +223,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
return
}

form := &apimodel.AdvancedStatusCreateForm{}
form := &apimodel.StatusCreateRequest{}
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
Expand Down Expand Up @@ -249,7 +262,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
// overlength inputs.
//
// Side effect: normalizes the post's language tag.
func validateNormalizeCreateStatus(form *apimodel.AdvancedStatusCreateForm) error {
func validateNormalizeCreateStatus(form *apimodel.StatusCreateRequest) error {
hasStatus := form.Status != ""
hasMedia := len(form.MediaIDs) != 0
hasPoll := form.Poll != nil
Expand Down Expand Up @@ -286,10 +299,16 @@ func validateNormalizeCreateStatus(form *apimodel.AdvancedStatusCreateForm) erro
form.Language = language
}

// Check if the deprecated "federated" field was
// set in lieu of "local_only", and use it if so.
if form.LocalOnly == nil && form.Federated != nil { // nolint:staticcheck
form.LocalOnly = util.Ptr(!*form.Federated) // nolint:staticcheck
}

return nil
}

func validateNormalizeCreatePoll(form *apimodel.AdvancedStatusCreateForm) error {
func validateNormalizeCreatePoll(form *apimodel.StatusCreateRequest) error {
maxPollOptions := config.GetStatusesPollMaxOptions()
maxPollChars := config.GetStatusesPollOptionMaxChars()

Expand Down
24 changes: 6 additions & 18 deletions internal/api/model/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type Status struct {
// Visibility of this status.
// example: unlisted
Visibility Visibility `json:"visibility"`
// Set to "true" if status is not federated, ie., a "local only" status; omitted from response otherwise.
LocalOnly bool `json:"local_only,omitempty"`
// Primary language of this status (ISO 639 Part 1 two-letter language code).
// Will be null if language is not known.
// example: en
Expand Down Expand Up @@ -209,6 +211,10 @@ type StatusCreateRequest struct {
SpoilerText string `form:"spoiler_text" json:"spoiler_text" xml:"spoiler_text"`
// Visibility of the posted status.
Visibility Visibility `form:"visibility" json:"visibility" xml:"visibility"`
// Set to "true" if this status should not be federated, ie. it should be a "local only" status.
LocalOnly *bool `form:"local_only"`
// Deprecated: Only used if LocalOnly is not set.
Federated *bool `form:"federated"`
// ISO 8601 Datetime at which to schedule a status.
// Providing this parameter will cause ScheduledStatus to be returned instead of Status.
// Must be at least 5 minutes in the future.
Expand Down Expand Up @@ -238,24 +244,6 @@ const (
VisibilityDirect Visibility = "direct"
)

// AdvancedStatusCreateForm wraps the mastodon-compatible status create form along with the GTS advanced
// visibility settings.
//
// swagger:ignore
type AdvancedStatusCreateForm struct {
StatusCreateRequest
AdvancedVisibilityFlagsForm
}

// AdvancedVisibilityFlagsForm allows a few more advanced flags to be set on new statuses, in addition
// to the standard mastodon-compatible ones.
//
// swagger:ignore
type AdvancedVisibilityFlagsForm struct {
// This status will be federated beyond the local timeline(s).
Federated *bool `form:"federated" json:"federated" xml:"federated"`
}

// StatusContentType is the content type with which to parse the submitted status.
// Can be either text/plain or text/markdown. Empty will default to text/plain.
//
Expand Down
2 changes: 1 addition & 1 deletion internal/db/bundb/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogs()
func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogsPublicOnly() {
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, true)
suite.NoError(err)
suite.Len(statuses, 2)
suite.Len(statuses, 3)
}

// populateTestStatus adds mandatory fields to a partially populated status.
Expand Down
32 changes: 22 additions & 10 deletions internal/filter/visibility/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ func (f *Filter) isStatusVisible(
}

// Check whether status accounts are visible to the requester.
visible, err := f.areStatusAccountsVisible(ctx, requester, status)
acctsVisible, err := f.areStatusAccountsVisible(ctx, requester, status)
if err != nil {
return false, gtserror.Newf("error checking status %s account visibility: %w", status.ID, err)
} else if !visible {
} else if !acctsVisible {
return false, nil
}

Expand All @@ -112,22 +112,34 @@ func (f *Filter) isStatusVisible(
)
}

if status.Visibility == gtsmodel.VisibilityPublic {
// This status will be visible to all.
return true, nil
if requester == nil {
// The request is unauthed. Only federated, Public statuses are visible without auth.
visibleUnauthed := !status.IsLocalOnly() && status.Visibility == gtsmodel.VisibilityPublic
return visibleUnauthed, nil
}

if requester == nil {
// This request is WITHOUT auth, and status is NOT public.
log.Trace(ctx, "unauthorized request to non-public status")
/*
From this point down we know the request is authed.
*/

if requester.IsRemote() && status.IsLocalOnly() {
// Remote accounts can't see local-only
// posts regardless of their visibility.
return false, nil
}

if status.Visibility == gtsmodel.VisibilityUnlocked {
// This status is visible to all auth'd accounts.
if status.Visibility == gtsmodel.VisibilityPublic ||
status.Visibility == gtsmodel.VisibilityUnlocked {
// This status is visible to all auth'd accounts
// (pending blocks, which we already checked above).
return true, nil
}

/*
From this point down we know the request
is of visibility followers-only or below.
*/

if requester.ID == status.AccountID {
// Author can always see their own status.
return true, nil
Expand Down
37 changes: 37 additions & 0 deletions internal/filter/visibility/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,43 @@ func (suite *StatusVisibleTestSuite) TestVisiblePending() {
}
}

func (suite *StatusVisibleTestSuite) TestVisibleLocalOnly() {
ctx := context.Background()

// Local-only, Public status.
testStatus := suite.testStatuses["local_account_2_status_4"]

for _, testCase := range []struct {
acct *gtsmodel.Account
visible bool
}{
{
acct: suite.testAccounts["local_account_2"],
visible: true, // Own status, always visible.
},
{
acct: nil,
visible: false, // No auth, should not be visible..
},
{
acct: suite.testAccounts["local_account_1"],
visible: true, // Local account, should be visible.
},
{
acct: suite.testAccounts["remote_account_1"],
visible: false, // Blocked account, should not be visible.
},
{
acct: suite.testAccounts["remote_account_2"],
visible: false, // Remote account, should not be visible.
},
} {
visible, err := suite.filter.StatusVisible(ctx, testCase.acct, testStatus)
suite.NoError(err)
suite.Equal(testCase.visible, visible)
}
}

func TestStatusVisibleTestSuite(t *testing.T) {
suite.Run(t, new(StatusVisibleTestSuite))
}
6 changes: 6 additions & 0 deletions internal/gtsmodel/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ func (s *Status) IsLocal() bool {
return s.Local != nil && *s.Local
}

// IsLocalOnly returns true if this status
// is "local-only" ie., unfederated.
func (s *Status) IsLocalOnly() bool {
return s.Federated == nil || !*s.Federated
}

// StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags.
type StatusToTag struct {
StatusID string `bun:"type:CHAR(26),unique:statustag,nullzero,notnull"`
Expand Down
12 changes: 12 additions & 0 deletions internal/processing/fedi/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ func (p *Processor) OutboxGet(
hi = statuses[0].ID
}

// Reslice statuses dropping all those invisible to requester
// (eg., local-only statuses, if the requester is remote).
statuses, err = p.visFilter.StatusesVisible(
ctx,
auth.requestingAcct,
statuses,
)
if err != nil {
err := gtserror.Newf("error filtering statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}

// Start building AS collection page params.
params.Total = util.Ptr(*receivingAcct.Stats.StatusesCount)
var pageParams ap.CollectionPageParams
Expand Down
20 changes: 10 additions & 10 deletions internal/processing/status/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (p *Processor) Create(
ctx context.Context,
requester *gtsmodel.Account,
application *gtsmodel.Application,
form *apimodel.AdvancedStatusCreateForm,
form *apimodel.StatusCreateRequest,
) (
*apimodel.Status,
gtserror.WithCode,
Expand Down Expand Up @@ -290,7 +290,7 @@ func (p *Processor) processThreadID(ctx context.Context, status *gtsmodel.Status
return nil
}

func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.StatusCreateRequest, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.MediaIDs == nil {
return nil
}
Expand Down Expand Up @@ -338,7 +338,7 @@ func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.Advanced
}

func processVisibility(
form *apimodel.AdvancedStatusCreateForm,
form *apimodel.StatusCreateRequest,
accountDefaultVis gtsmodel.Visibility,
status *gtsmodel.Status,
) error {
Expand All @@ -356,16 +356,16 @@ func processVisibility(
status.Visibility = gtsmodel.VisibilityDefault
}

// Set federated flag to form value
// if provided, or default to true.
federated := util.PtrOrValue(form.Federated, true)
status.Federated = &federated
// Set federated according to "local_only" field,
// assuming federated (ie., not local-only) by default.
localOnly := util.PtrOrValue(form.LocalOnly, false)
status.Federated = util.Ptr(!localOnly)

return nil
}

func processInteractionPolicy(
_ *apimodel.AdvancedStatusCreateForm,
_ *apimodel.StatusCreateRequest,
settings *gtsmodel.AccountSettings,
status *gtsmodel.Status,
) error {
Expand Down Expand Up @@ -413,7 +413,7 @@ func processInteractionPolicy(
return nil
}

func processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
func processLanguage(form *apimodel.StatusCreateRequest, accountDefaultLanguage string, status *gtsmodel.Status) error {
if form.Language != "" {
status.Language = form.Language
} else {
Expand All @@ -425,7 +425,7 @@ func processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLang
return nil
}

func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, status *gtsmodel.Status) error {
func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.StatusCreateRequest, status *gtsmodel.Status) error {
if form.ContentType == "" {
// If content type wasn't specified, use the author's preferred content-type.
contentType := apimodel.StatusContentType(status.Account.Settings.StatusContentType)
Expand Down
Loading

0 comments on commit 53fccb8

Please sign in to comment.