diff options
Diffstat (limited to 'internal')
20 files changed, 616 insertions, 98 deletions
diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index faf793bd8..ec961f80b 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -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 diff --git a/internal/ap/properties.go b/internal/ap/properties.go index 722c3fca5..3e064bae0 100644 --- a/internal/ap/properties.go +++ b/internal/ap/properties.go @@ -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 { diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 66dc3b307..603740f17 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -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 diff --git a/internal/db/bundb/admin.go b/internal/db/bundb/admin.go index 0666cf8a8..dcf51c6a5 100644 --- a/internal/db/bundb/admin.go +++ b/internal/db/bundb/admin.go @@ -120,20 +120,21 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) ( } account = >smodel.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! diff --git a/internal/db/bundb/migrations/20250708074906_unauthed_web_updates.go b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates.go new file mode 100644 index 000000000..f69dbac86 --- /dev/null +++ b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates.go @@ -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) + } +} diff --git a/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/common/visibility.go b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/common/visibility.go new file mode 100644 index 000000000..51cb13d87 --- /dev/null +++ b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/common/visibility.go @@ -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 +) diff --git a/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/new/account.go b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/new/account.go new file mode 100644 index 000000000..ad25a5bc9 --- /dev/null +++ b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/new/account.go @@ -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"` +} diff --git a/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/old/account.go b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/old/account.go new file mode 100644 index 000000000..0086b1464 --- /dev/null +++ b/internal/db/bundb/migrations/20250708074906_unauthed_web_updates/old/account.go @@ -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:""` +} diff --git a/internal/filter/visibility/status.go b/internal/filter/visibility/status.go index 24fa6f2e6..c46fd369c 100644 --- a/internal/filter/visibility/status.go +++ b/internal/filter/visibility/status.go @@ -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 - } - - // 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, - ) - } + return false } - switch webvis := status.Account.Settings.WebVisibility; webvis { + switch status.Visibility { - // 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 } } diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 664f1f66a..8b2de6b23 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -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). diff --git a/internal/gtsmodel/accountsettings.go b/internal/gtsmodel/accountsettings.go index 30fb7e5df..9fa05e139 100644 --- a/internal/gtsmodel/accountsettings.go +++ b/internal/gtsmodel/accountsettings.go @@ -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. diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index f0e3b790b..99dd074a5 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -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 { diff --git a/internal/processing/timeline/public_test.go b/internal/processing/timeline/public_test.go index 3320a45da..cf3ade7e5 100644 --- a/internal/processing/timeline/public_test.go +++ b/internal/processing/timeline/public_test.go @@ -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. diff --git a/internal/transport/dereference_test.go b/internal/transport/dereference_test.go index b9611d1e7..836ba2ba5 100644 --- a/internal/transport/dereference_test.go +++ b/internal/transport/dereference_test.go @@ -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", diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 35f1e7cb7..ede2b3253 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -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 } diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 3feecaa9b..ce1501e1a 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -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 } diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index 5da103582..f3e19bb81 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -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", diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 3b5af6579..aef38ad6e 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -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, diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 8b0d15f10..a26ff114e 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -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", diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go index 828e6a683..c0c51b37a 100644 --- a/internal/typeutils/wrap_test.go +++ b/internal/typeutils/wrap_test.go @@ -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", |
