[feature] Use hidesToPublicFromUnauthedWeb and hidesCcPublicFromUnauthedWeb properties for web visibility of statuses (#4315)

This pull request implements two new properties on ActivityPub actors: `hidesToPublicFromUnauthedWeb` and `hidesCcPublicFromUnauthedWeb`.

As documented, these properties allow actors to signal their preference for whether or not their posts should be hidden from unauthenticated web views (ie., web pages like the GtS frontend, web apps like the Mastodon frontend, web APIs like the Mastodon public timeline API, etc). This allows remote accounts to *opt in* to having their unlisted visibility posts shown in (for example) the replies section of the web view of a GtS thread. In future, we can also use these properties to determine whether we should show boosts of a remote actor's post on a GtS profile, and that sort of thing.

In keeping with our stance around privacy by default, GtS assumes `true` for `hidesCcPublicFromUnauthedWeb` if the property is not set on a remote actor, ie., hide unlisted/unlocked posts by default. `hidesToPublicFromUnauthedWeb` is assumed to be `false` if the property is not set on a remote actor, ie., show public posts by default.

~~WIP as I still want to work on the documentation for this a bit.~~

New props are already in the namespace document: https://gotosocial.org/ns

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4315
Reviewed-by: kim <gruf@noreply.codeberg.org>
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
tobi 2025-07-09 16:50:25 +02:00 committed by kim
commit dcfc9b7885
159 changed files with 10900 additions and 2918 deletions

View file

@ -227,6 +227,8 @@ type Accountable interface {
WithMovedTo
WithAlsoKnownAs
WithManuallyApprovesFollowers
WithHidesToPublicFromUnauthedWeb
WithHidesCcPublicFromUnauthedWeb
WithEndpoints
WithTag
WithPublished
@ -711,6 +713,18 @@ type WithManuallyApprovesFollowers interface {
SetActivityStreamsManuallyApprovesFollowers(vocab.ActivityStreamsManuallyApprovesFollowersProperty)
}
// WithHidesToPublicFromUnauthedWeb represents a Person or profile with the hidesToPublicFromUnauthedWeb property.
type WithHidesToPublicFromUnauthedWeb interface {
GetGoToSocialHidesToPublicFromUnauthedWeb() vocab.GoToSocialHidesToPublicFromUnauthedWebProperty
SetGoToSocialHidesToPublicFromUnauthedWeb(vocab.GoToSocialHidesToPublicFromUnauthedWebProperty)
}
// WithHidesCcPublicFromUnauthedWeb represents a Person or profile with the hidesCcPublicFromUnauthedWeb property.
type WithHidesCcPublicFromUnauthedWeb interface {
GetGoToSocialHidesCcPublicFromUnauthedWeb() vocab.GoToSocialHidesCcPublicFromUnauthedWebProperty
SetGoToSocialHidesCcPublicFromUnauthedWeb(vocab.GoToSocialHidesCcPublicFromUnauthedWebProperty)
}
// WithEndpoints represents a Person or profile with the endpoints property
type WithEndpoints interface {
GetActivityStreamsEndpoints() vocab.ActivityStreamsEndpointsProperty

View file

@ -562,6 +562,48 @@ func SetManuallyApprovesFollowers(with WithManuallyApprovesFollowers, manuallyAp
mafProp.Set(manuallyApprovesFollowers)
}
// GetHidesToPublicFromUnauthedWeb returns the boolean contained in the hidesToPublicFromUnauthedWeb property of 'with'.
//
// Returns default 'false' if property unusable or not set.
func GetHidesToPublicFromUnauthedWeb(with WithHidesToPublicFromUnauthedWeb) bool {
hidesProp := with.GetGoToSocialHidesToPublicFromUnauthedWeb()
if hidesProp == nil || !hidesProp.IsXMLSchemaBoolean() {
return false
}
return hidesProp.Get()
}
// SetHidesToPublicFromUnauthedWeb sets the given boolean on the hidesToPublicFromUnauthedWeb property of 'with'.
func SetHidesToPublicFromUnauthedWeb(with WithHidesToPublicFromUnauthedWeb, hidesToPublicFromUnauthedWeb bool) {
hidesProp := with.GetGoToSocialHidesToPublicFromUnauthedWeb()
if hidesProp == nil {
hidesProp = streams.NewGoToSocialHidesToPublicFromUnauthedWebProperty()
with.SetGoToSocialHidesToPublicFromUnauthedWeb(hidesProp)
}
hidesProp.Set(hidesToPublicFromUnauthedWeb)
}
// GetHidesCcPublicFromUnauthedWeb returns the boolean contained in the hidesCcPublicFromUnauthedWeb property of 'with'.
//
// Returns default 'true' if property unusable or not set.
func GetHidesCcPublicFromUnauthedWeb(with WithHidesCcPublicFromUnauthedWeb) bool {
hidesProp := with.GetGoToSocialHidesCcPublicFromUnauthedWeb()
if hidesProp == nil || !hidesProp.IsXMLSchemaBoolean() {
return true
}
return hidesProp.Get()
}
// SetHidesCcPublicFromUnauthedWeb sets the given boolean on the hidesCcPublicFromUnauthedWeb property of 'with'.
func SetHidesCcPublicFromUnauthedWeb(with WithHidesCcPublicFromUnauthedWeb, hidesCcPublicFromUnauthedWeb bool) {
hidesProp := with.GetGoToSocialHidesCcPublicFromUnauthedWeb()
if hidesProp == nil {
hidesProp = streams.NewGoToSocialHidesCcPublicFromUnauthedWebProperty()
with.SetGoToSocialHidesCcPublicFromUnauthedWeb(hidesProp)
}
hidesProp.Set(hidesCcPublicFromUnauthedWeb)
}
// GetApprovedBy returns the URL contained in
// the ApprovedBy property of 'with', if set.
func GetApprovedBy(with WithApprovedBy) *url.URL {

View file

@ -1054,10 +1054,21 @@ func (a *accountDB) GetAccountWebStatuses(
return nil, nil
}
// Check for an easy case: account exposes no statuses via the web.
webVisibility := account.Settings.WebVisibility
if webVisibility == gtsmodel.VisibilityNone {
return nil, db.ErrNoEntries
// Derive visibility of statuses on the web.
//
// We don't account for situations where someone
// hides public statuses but shows unlocked/unlisted,
// since that's only an option for remote accts.
var (
hideAll = *account.HidesToPublicFromUnauthedWeb
publicOnly = *account.HidesCcPublicFromUnauthedWeb
)
if hideAll {
// Account hides all
// statuses from web,
// nothing to do.
return nil, nil
}
// Ensure reasonable
@ -1075,27 +1086,18 @@ func (a *accountDB) GetAccountWebStatuses(
Column("status.id").
Where("? = ?", bun.Ident("status.account_id"), account.ID)
// Select statuses for this account according
// to their web visibility preference.
switch webVisibility {
case gtsmodel.VisibilityPublic:
// Select statuses according to
// account's web visibility prefs.
if publicOnly {
// Only Public statuses.
q = q.Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic)
case gtsmodel.VisibilityUnlocked:
} else {
// Public or Unlocked.
visis := []gtsmodel.Visibility{
gtsmodel.VisibilityPublic,
gtsmodel.VisibilityUnlocked,
}
q = q.Where("? IN (?)", bun.Ident("status.visibility"), bun.In(visis))
default:
return nil, gtserror.Newf(
"unrecognized web visibility for account %s: %s",
account.ID, webVisibility,
)
}
// Don't show replies, boosts, or

View file

@ -120,20 +120,21 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
}
account = &gtsmodel.Account{
ID: accountID,
Username: newSignup.Username,
DisplayName: newSignup.Username,
URI: uris.UserURI,
URL: uris.UserURL,
InboxURI: uris.InboxURI,
OutboxURI: uris.OutboxURI,
FollowingURI: uris.FollowingURI,
FollowersURI: uris.FollowersURI,
FeaturedCollectionURI: uris.FeaturedCollectionURI,
ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: privKey,
PublicKey: &privKey.PublicKey,
PublicKeyURI: uris.PublicKeyURI,
ID: accountID,
Username: newSignup.Username,
DisplayName: newSignup.Username,
URI: uris.UserURI,
URL: uris.UserURL,
InboxURI: uris.InboxURI,
OutboxURI: uris.OutboxURI,
FollowingURI: uris.FollowingURI,
FollowersURI: uris.FollowersURI,
FeaturedCollectionURI: uris.FeaturedCollectionURI,
ActorType: gtsmodel.AccountActorTypePerson,
PrivateKey: privKey,
PublicKey: &privKey.PublicKey,
PublicKeyURI: uris.PublicKeyURI,
HidesCcPublicFromUnauthedWeb: util.Ptr(true), // GtS default to hide unlisted.
}
// Insert the new account!

View file

@ -0,0 +1,164 @@
// 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"
"fmt"
"reflect"
"code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/common"
newmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/new"
oldmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/old"
"code.superseriousbusiness.org/gotosocial/internal/log"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
var account *newmodel.Account
accountType := reflect.TypeOf(account)
// Add new columns to accounts
// table if they don't exist already.
for _, new := range []struct {
dbCol string
fieldName string
}{
{
dbCol: "hides_to_public_from_unauthed_web",
fieldName: "HidesToPublicFromUnauthedWeb",
},
{
dbCol: "hides_cc_public_from_unauthed_web",
fieldName: "HidesCcPublicFromUnauthedWeb",
},
} {
exists, err := doesColumnExist(
ctx,
tx,
"accounts",
new.dbCol,
)
if err != nil {
return err
}
if exists {
// Column already exists.
continue
}
// Column doesn't exist yet, add it.
colDef, err := getBunColumnDef(tx, accountType, new.fieldName)
if err != nil {
return fmt.Errorf("error making column def: %w", err)
}
log.Infof(ctx, "adding accounts.%s column...", new.dbCol)
if _, err := tx.
NewAddColumn().
Model(account).
ColumnExpr(colDef).
Exec(ctx); err != nil {
return fmt.Errorf("error adding column: %w", err)
}
}
// For each account settings we have
// stored on this instance, set the
// new account columns to values
// corresponding to the setting.
allSettings := []*oldmodel.AccountSettings{}
if err := tx.
NewSelect().
Model(&allSettings).
Column("account_id", "web_visibility").
Scan(ctx); err != nil {
return fmt.Errorf("error selecting settings: %w", err)
}
for _, settings := range allSettings {
// Derive web visibility.
var (
hidesToPublicFromUnauthedWeb bool
hidesCcPublicFromUnauthedWeb bool
)
switch settings.WebVisibility {
// Show nothing.
case common.VisibilityNone:
hidesToPublicFromUnauthedWeb = true
hidesCcPublicFromUnauthedWeb = true
// Show public only (GtS default).
case common.VisibilityPublic:
hidesToPublicFromUnauthedWeb = false
hidesCcPublicFromUnauthedWeb = true
// Show public + unlisted (Masto default).
case common.VisibilityUnlocked:
hidesToPublicFromUnauthedWeb = false
hidesCcPublicFromUnauthedWeb = false
default:
log.Warnf(ctx,
"local account %s had unrecognized settings.WebVisibility %d, skipping...",
settings.AccountID, settings.WebVisibility,
)
continue
}
// Update account.
if _, err := tx.
NewUpdate().
Table("accounts").
Set("? = ?", bun.Ident("hides_to_public_from_unauthed_web"), hidesToPublicFromUnauthedWeb).
Set("? = ?", bun.Ident("hides_cc_public_from_unauthed_web"), hidesCcPublicFromUnauthedWeb).
Where("? = ?", bun.Ident("id"), settings.AccountID).Exec(ctx); err != nil {
return fmt.Errorf("error updating local account: %w", err)
}
}
// Drop the old web_visibility column.
if _, err := tx.
NewDropColumn().
Model((*oldmodel.AccountSettings)(nil)).
Column("web_visibility").
Exec(ctx); err != nil {
return fmt.Errorf("error dropping old web_visibility column: %w", err)
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -0,0 +1,50 @@
// 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 common
// Visibility represents the
// visibility granularity of a status.
type Visibility int16
const (
// VisibilityNone means nobody can see this.
// It's only used for web status visibility.
VisibilityNone Visibility = 1
// VisibilityPublic means this status will
// be visible to everyone on all timelines.
VisibilityPublic Visibility = 2
// VisibilityUnlocked means this status will be visible to everyone,
// but will only show on home timeline to followers, and in lists.
VisibilityUnlocked Visibility = 3
// VisibilityFollowersOnly means this status is viewable to followers only.
VisibilityFollowersOnly Visibility = 4
// VisibilityMutualsOnly means this status
// is visible to mutual followers only.
VisibilityMutualsOnly Visibility = 5
// VisibilityDirect means this status is
// visible only to mentioned recipients.
VisibilityDirect Visibility = 6
// VisibilityDefault is used when no other setting can be found.
VisibilityDefault Visibility = VisibilityUnlocked
)

View file

@ -0,0 +1,102 @@
// 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 (
"crypto/rsa"
"time"
"code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/common"
)
type Account 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"`
Username string `bun:",nullzero,notnull,unique:accounts_username_domain_uniq"`
Domain string `bun:",nullzero,unique:accounts_username_domain_uniq"`
AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
AvatarRemoteURL string `bun:",nullzero"`
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
HeaderRemoteURL string `bun:",nullzero"`
DisplayName string `bun:",nullzero"`
EmojiIDs []string `bun:"emojis,array"`
Fields []*Field `bun:",nullzero"`
FieldsRaw []*Field `bun:",nullzero"`
Note string `bun:",nullzero"`
NoteRaw string `bun:",nullzero"`
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
AlsoKnownAs []*Account `bun:"-"`
MovedToURI string `bun:",nullzero"`
MovedTo *Account `bun:"-"`
MoveID string `bun:"type:CHAR(26),nullzero"`
Locked *bool `bun:",nullzero,notnull,default:true"`
Discoverable *bool `bun:",nullzero,notnull,default:false"`
URI string `bun:",nullzero,notnull,unique"`
URL string `bun:",nullzero"`
InboxURI string `bun:",nullzero"`
SharedInboxURI *string `bun:""`
OutboxURI string `bun:",nullzero"`
FollowingURI string `bun:",nullzero"`
FollowersURI string `bun:",nullzero"`
FeaturedCollectionURI string `bun:",nullzero"`
ActorType int16 `bun:",nullzero,notnull"`
PrivateKey *rsa.PrivateKey `bun:""`
PublicKey *rsa.PublicKey `bun:",notnull"`
PublicKeyURI string `bun:",nullzero,notnull,unique"`
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
MemorializedAt time.Time `bun:"type:timestamptz,nullzero"`
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
// Added in this migration:
HidesToPublicFromUnauthedWeb *bool `bun:",nullzero,notnull,default:false"`
HidesCcPublicFromUnauthedWeb *bool `bun:",nullzero,notnull,default:false"`
}
type Field struct {
Name string
Value string
VerifiedAt time.Time `bun:",nullzero"`
}
type AccountSettings struct {
AccountID 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"`
Privacy common.Visibility `bun:",nullzero,default:3"`
Sensitive *bool `bun:",nullzero,notnull,default:false"`
Language string `bun:",nullzero,notnull,default:'en'"`
StatusContentType string `bun:",nullzero"`
Theme string `bun:",nullzero"`
CustomCSS string `bun:",nullzero"`
EnableRSS *bool `bun:",nullzero,notnull,default:false"`
HideCollections *bool `bun:",nullzero,notnull,default:false"`
WebLayout int16 `bun:",nullzero,notnull,default:1"`
InteractionPolicyDirect *struct{} `bun:""`
InteractionPolicyMutualsOnly *struct{} `bun:""`
InteractionPolicyFollowersOnly *struct{} `bun:""`
InteractionPolicyUnlocked *struct{} `bun:""`
InteractionPolicyPublic *struct{} `bun:""`
// Removed in this migration:
// WebVisibility common.Visibility `bun:",nullzero,notnull,default:3"`
}

View file

@ -0,0 +1,96 @@
// 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 (
"crypto/rsa"
"time"
"code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/common"
)
type Account 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"`
Username string `bun:",nullzero,notnull,unique:accounts_username_domain_uniq"`
Domain string `bun:",nullzero,unique:accounts_username_domain_uniq"`
AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
AvatarRemoteURL string `bun:",nullzero"`
HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"`
HeaderRemoteURL string `bun:",nullzero"`
DisplayName string `bun:",nullzero"`
EmojiIDs []string `bun:"emojis,array"`
Fields []*Field `bun:",nullzero"`
FieldsRaw []*Field `bun:",nullzero"`
Note string `bun:",nullzero"`
NoteRaw string `bun:",nullzero"`
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"`
AlsoKnownAs []*Account `bun:"-"`
MovedToURI string `bun:",nullzero"`
MovedTo *Account `bun:"-"`
MoveID string `bun:"type:CHAR(26),nullzero"`
Locked *bool `bun:",nullzero,notnull,default:true"`
Discoverable *bool `bun:",nullzero,notnull,default:false"`
URI string `bun:",nullzero,notnull,unique"`
URL string `bun:",nullzero"`
InboxURI string `bun:",nullzero"`
SharedInboxURI *string `bun:""`
OutboxURI string `bun:",nullzero"`
FollowingURI string `bun:",nullzero"`
FollowersURI string `bun:",nullzero"`
FeaturedCollectionURI string `bun:",nullzero"`
ActorType int16 `bun:",nullzero,notnull"`
PrivateKey *rsa.PrivateKey `bun:""`
PublicKey *rsa.PublicKey `bun:",notnull"`
PublicKeyURI string `bun:",nullzero,notnull,unique"`
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"`
MemorializedAt time.Time `bun:"type:timestamptz,nullzero"`
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"`
SilencedAt time.Time `bun:"type:timestamptz,nullzero"`
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"`
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"`
}
type Field struct {
Name string
Value string
VerifiedAt time.Time `bun:",nullzero"`
}
type AccountSettings struct {
AccountID 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"`
Privacy common.Visibility `bun:",nullzero,default:3"`
Sensitive *bool `bun:",nullzero,notnull,default:false"`
Language string `bun:",nullzero,notnull,default:'en'"`
StatusContentType string `bun:",nullzero"`
Theme string `bun:",nullzero"`
CustomCSS string `bun:",nullzero"`
EnableRSS *bool `bun:",nullzero,notnull,default:false"`
HideCollections *bool `bun:",nullzero,notnull,default:false"`
WebVisibility common.Visibility `bun:",nullzero,notnull,default:3"`
WebLayout int16 `bun:",nullzero,notnull,default:1"`
InteractionPolicyDirect *struct{} `bun:""`
InteractionPolicyMutualsOnly *struct{} `bun:""`
InteractionPolicyFollowersOnly *struct{} `bun:""`
InteractionPolicyUnlocked *struct{} `bun:""`
InteractionPolicyPublic *struct{} `bun:""`
}

View file

@ -115,9 +115,7 @@ func (f *Filter) isStatusVisible(
if requester == nil {
// Use a different visibility
// heuristic for unauthed requests.
return f.isStatusVisibleUnauthed(
ctx, status,
)
return f.isStatusVisibleUnauthed(status), nil
}
/*
@ -245,57 +243,29 @@ func isPendingStatusVisible(requester *gtsmodel.Account, status *gtsmodel.Status
return false
}
// isStatusVisibleUnauthed returns whether status is visible without any unauthenticated account.
func (f *Filter) isStatusVisibleUnauthed(ctx context.Context, status *gtsmodel.Status) (bool, error) {
// For remote accounts, only show
// Public statuses via the web.
if status.Account.IsRemote() {
return status.Visibility == gtsmodel.VisibilityPublic, nil
}
// isStatusVisibleUnauthed returns whether status is visible without authentication.
func (f *Filter) isStatusVisibleUnauthed(status *gtsmodel.Status) bool {
// If status is local only,
// never show via the web.
// never show without auth.
if status.IsLocalOnly() {
return false, nil
return false
}
// Check account's settings to see
// what they expose. Populate these
// from the DB if necessary.
if status.Account.Settings == nil {
var err error
status.Account.Settings, err = f.state.DB.GetAccountSettings(ctx, status.Account.ID)
if err != nil {
return false, gtserror.Newf(
"error getting settings for account %s: %w",
status.Account.ID, err,
)
}
}
switch status.Visibility {
switch webvis := status.Account.Settings.WebVisibility; webvis {
// public_only: status must be Public.
case gtsmodel.VisibilityPublic:
return status.Visibility == gtsmodel.VisibilityPublic, nil
// Visible if account doesn't hide Public statuses.
return !*status.Account.HidesToPublicFromUnauthedWeb
// unlisted: status must be Public or Unlocked.
case gtsmodel.VisibilityUnlocked:
visible := status.Visibility == gtsmodel.VisibilityPublic ||
status.Visibility == gtsmodel.VisibilityUnlocked
return visible, nil
// Visible if account doesn't hide Unlocked statuses.
return !*status.Account.HidesCcPublicFromUnauthedWeb
// none: never show via the web.
case gtsmodel.VisibilityNone:
return false, nil
// Huh?
default:
return false, gtserror.Newf(
"unrecognized web visibility for account %s: %s",
status.Account.ID, webvis,
)
// For all other visibilities,
// never show without auth.
return false
}
}

View file

@ -272,6 +272,16 @@ type Account struct {
//
// Local accounts only.
Stats *AccountStats `bun:"-"`
// True if the actor hides to-public statusables
// from unauthenticated public access via the web.
// Default "false" if not set on the actor model.
HidesToPublicFromUnauthedWeb *bool `bun:",nullzero,notnull,default:false"`
// True if the actor hides cc-public statusables
// from unauthenticated public access via the web.
// Default "true" if not set on the actor model.
HidesCcPublicFromUnauthedWeb *bool `bun:",nullzero,notnull,default:true"`
}
// UsernameDomain returns account @username@domain (missing domain if local).

View file

@ -35,7 +35,6 @@ 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.
WebVisibility Visibility `bun:",nullzero,notnull,default:3"` // Visibility level of statuses that visitors can view via the web profile.
WebLayout WebLayout `bun:",nullzero,notnull,default:1"` // Layout to use when showing this profile via the web.
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.

View file

@ -212,6 +212,37 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
}
if form.WebVisibility != nil {
switch apimodel.Visibility(*form.WebVisibility) {
// Show none.
case apimodel.VisibilityNone:
account.HidesToPublicFromUnauthedWeb = util.Ptr(true)
account.HidesCcPublicFromUnauthedWeb = util.Ptr(true)
// Show public only (GtS default).
case apimodel.VisibilityPublic:
account.HidesToPublicFromUnauthedWeb = util.Ptr(false)
account.HidesCcPublicFromUnauthedWeb = util.Ptr(true)
// Show public and unlisted (Masto default).
case apimodel.VisibilityUnlisted:
account.HidesToPublicFromUnauthedWeb = util.Ptr(false)
account.HidesCcPublicFromUnauthedWeb = util.Ptr(false)
default:
const text = "web_visibility must be one of public, unlisted, or none"
err := errors.New(text)
return nil, gtserror.NewErrorBadRequest(err, text)
}
acctColumns = append(
acctColumns,
"hides_to_public_from_unauthed_web",
"hides_cc_public_from_unauthed_web",
)
}
// Account settings flags.
if form.Source != nil {
@ -287,21 +318,6 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
settingsColumns = append(settingsColumns, "hide_collections")
}
if form.WebVisibility != nil {
apiVis := apimodel.Visibility(*form.WebVisibility)
webVisibility := typeutils.APIVisToVis(apiVis)
if webVisibility != gtsmodel.VisibilityPublic &&
webVisibility != gtsmodel.VisibilityUnlocked &&
webVisibility != gtsmodel.VisibilityNone {
const text = "web_visibility must be one of public, unlocked, or none"
err := errors.New(text)
return nil, gtserror.NewErrorBadRequest(err, text)
}
account.Settings.WebVisibility = webVisibility
settingsColumns = append(settingsColumns, "web_visibility")
}
if form.WebLayout != nil {
webLayout := gtsmodel.ParseWebLayout(*form.WebLayout)
if webLayout == gtsmodel.WebLayoutUnknown {

View file

@ -67,7 +67,7 @@ func (suite *PublicTestSuite) TestPublicTimelineGetNotEmpty() {
ctx = suite.T().Context()
requester = suite.testAccounts["local_account_1"]
// Select 1 *just above* a status we know should
// not be in the public timeline -- a public
// not be in the public timeline -- an unlisted
// reply to one of admin's statuses.
maxID = "01HE7XJ1CG84TBKH5V9XKBVGF6"
sinceID = ""
@ -91,9 +91,9 @@ func (suite *PublicTestSuite) TestPublicTimelineGetNotEmpty() {
// some other statuses were filtered out.
suite.NoError(errWithCode)
suite.Len(resp.Items, 1)
suite.Equal(`<http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV>; rel="next", <http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01HE7XJ1CG84TBKH5V9XKBVGF5>; rel="prev"`, resp.LinkHeader)
suite.Equal(`<http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV>; rel="next", <http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01FF25D5Q0DH7CHD57CTRS6WK0>; rel="prev"`, resp.LinkHeader)
suite.Equal(`http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV`, resp.NextLink)
suite.Equal(`http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01HE7XJ1CG84TBKH5V9XKBVGF5`, resp.PrevLink)
suite.Equal(`http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01FF25D5Q0DH7CHD57CTRS6WK0`, resp.PrevLink)
}
// A timeline containing a status hidden due to filtering should return other statuses with no error.

View file

@ -43,8 +43,8 @@ func (suite *DereferenceTestSuite) TestDerefLocalUser() {
defer resp.Body.Close()
suite.Equal(http.StatusOK, resp.StatusCode)
suite.EqualValues(2007, resp.ContentLength)
suite.Equal("2007", resp.Header.Get("Content-Length"))
suite.EqualValues(2109, resp.ContentLength)
suite.Equal("2109", resp.Header.Get("Content-Length"))
suite.Equal(apiutil.AppActivityLDJSON, resp.Header.Get("Content-Type"))
b, err := io.ReadAll(resp.Body)
@ -59,6 +59,7 @@ func (suite *DereferenceTestSuite) TestDerefLocalUser() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -75,6 +76,8 @@ func (suite *DereferenceTestSuite) TestDerefLocalUser() {
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
"following": "http://localhost:8080/users/the_mighty_zork/following",
"hidesCcPublicFromUnauthedWeb": false,
"hidesToPublicFromUnauthedWeb": false,
"icon": {
"mediaType": "image/jpeg",
"name": "a green goblin looking nasty",

View file

@ -244,6 +244,10 @@ func (c *Converter) ASRepresentationToAccount(
acct.PublicKey = pkey
acct.PublicKeyURI = pkeyURL.String()
// Web visibility for statuses.
acct.HidesToPublicFromUnauthedWeb = util.Ptr(ap.GetHidesToPublicFromUnauthedWeb(accountable))
acct.HidesCcPublicFromUnauthedWeb = util.Ptr(ap.GetHidesCcPublicFromUnauthedWeb(accountable))
return &acct, nil
}

View file

@ -399,6 +399,10 @@ func (c *Converter) AccountToAS(
}
}
// Web visibility for statuses.
ap.SetHidesToPublicFromUnauthedWeb(accountable, *a.HidesToPublicFromUnauthedWeb)
ap.SetHidesCcPublicFromUnauthedWeb(accountable, *a.HidesCcPublicFromUnauthedWeb)
return accountable, nil
}

View file

@ -48,6 +48,7 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -64,6 +65,8 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
"following": "http://localhost:8080/users/the_mighty_zork/following",
"hidesCcPublicFromUnauthedWeb": false,
"hidesToPublicFromUnauthedWeb": false,
"icon": {
"mediaType": "image/jpeg",
"name": "a green goblin looking nasty",
@ -116,6 +119,7 @@ func (suite *InternalToASTestSuite) TestAccountToASBot() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -132,6 +136,8 @@ func (suite *InternalToASTestSuite) TestAccountToASBot() {
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
"following": "http://localhost:8080/users/the_mighty_zork/following",
"hidesCcPublicFromUnauthedWeb": false,
"hidesToPublicFromUnauthedWeb": false,
"icon": {
"mediaType": "image/jpeg",
"name": "a green goblin looking nasty",
@ -178,6 +184,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -209,6 +216,8 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
"featured": "http://localhost:8080/users/1happyturtle/collections/featured",
"followers": "http://localhost:8080/users/1happyturtle/followers",
"following": "http://localhost:8080/users/1happyturtle/following",
"hidesCcPublicFromUnauthedWeb": true,
"hidesToPublicFromUnauthedWeb": false,
"id": "http://localhost:8080/users/1happyturtle",
"inbox": "http://localhost:8080/users/1happyturtle/inbox",
"manuallyApprovesFollowers": true,
@ -256,6 +265,7 @@ func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -279,6 +289,8 @@ func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() {
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
"following": "http://localhost:8080/users/the_mighty_zork/following",
"hidesCcPublicFromUnauthedWeb": false,
"hidesToPublicFromUnauthedWeb": false,
"icon": {
"mediaType": "image/jpeg",
"name": "a green goblin looking nasty",
@ -328,6 +340,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
// Despite only one field being set, attachments should still be a slice/array.
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -354,6 +367,8 @@ func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
"featured": "http://localhost:8080/users/1happyturtle/collections/featured",
"followers": "http://localhost:8080/users/1happyturtle/followers",
"following": "http://localhost:8080/users/1happyturtle/following",
"hidesCcPublicFromUnauthedWeb": true,
"hidesToPublicFromUnauthedWeb": false,
"id": "http://localhost:8080/users/1happyturtle",
"inbox": "http://localhost:8080/users/1happyturtle/inbox",
"manuallyApprovesFollowers": true,
@ -389,6 +404,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -406,6 +422,8 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
"following": "http://localhost:8080/users/the_mighty_zork/following",
"hidesCcPublicFromUnauthedWeb": false,
"hidesToPublicFromUnauthedWeb": false,
"icon": {
"mediaType": "image/jpeg",
"name": "a green goblin looking nasty",
@ -464,6 +482,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -483,6 +502,8 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
"following": "http://localhost:8080/users/the_mighty_zork/following",
"hidesCcPublicFromUnauthedWeb": false,
"hidesToPublicFromUnauthedWeb": false,
"icon": {
"mediaType": "image/jpeg",
"name": "a green goblin looking nasty",

View file

@ -134,9 +134,26 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
statusContentType = a.Settings.StatusContentType
}
// Derive web visibility for
// this local account's statuses.
var webVisibility apimodel.Visibility
switch {
case *a.HidesToPublicFromUnauthedWeb:
// Hides all.
webVisibility = apimodel.VisibilityNone
case !*a.HidesCcPublicFromUnauthedWeb:
// Shows unlisted + public (Masto default).
webVisibility = apimodel.VisibilityUnlisted
default:
// Shows public only (GtS default).
webVisibility = apimodel.VisibilityPublic
}
apiAccount.Source = &apimodel.Source{
Privacy: VisToAPIVis(a.Settings.Privacy),
WebVisibility: VisToAPIVis(a.Settings.WebVisibility),
WebVisibility: webVisibility,
WebLayout: a.Settings.WebLayout.String(),
Sensitive: *a.Settings.Sensitive,
Language: a.Settings.Language,

View file

@ -965,7 +965,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments
"in_reply_to_account_id": "01F8MH17FWEB39HZJ76B6VXSKF",
"sensitive": true,
"spoiler_text": "some unknown media included",
"visibility": "public",
"visibility": "unlisted",
"language": "en",
"uri": "http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5",
"url": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5",
@ -1114,7 +1114,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"in_reply_to_account_id": "01F8MH17FWEB39HZJ76B6VXSKF",
"sensitive": true,
"spoiler_text": "some unknown media included",
"visibility": "public",
"visibility": "unlisted",
"language": "en",
"uri": "http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5",
"url": "http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5",

View file

@ -178,6 +178,7 @@ func (suite *WrapTestSuite) TestWrapAccountableInUpdate() {
suite.Equal(`{
"@context": [
"https://gotosocial.org/ns",
"https://w3id.org/security/v1",
"https://www.w3.org/ns/activitystreams",
{
@ -198,6 +199,8 @@ func (suite *WrapTestSuite) TestWrapAccountableInUpdate() {
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
"following": "http://localhost:8080/users/the_mighty_zork/following",
"hidesCcPublicFromUnauthedWeb": false,
"hidesToPublicFromUnauthedWeb": false,
"icon": {
"mediaType": "image/jpeg",
"name": "a green goblin looking nasty",