diff options
| author | 2025-07-09 16:50:25 +0200 | |
|---|---|---|
| committer | 2025-07-09 16:50:25 +0200 | |
| commit | dcfc9b7885e7ed4f7886a35ccb3e007c293d3521 (patch) | |
| tree | 3bef2a25b9c8178bdfbece05a5165c94a5a7ae03 /internal/db | |
| parent | [performance] use our own typed value context types for Value() key checking ... (diff) | |
| download | gotosocial-dcfc9b7885e7ed4f7886a35ccb3e007c293d3521.tar.xz | |
[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>
Diffstat (limited to 'internal/db')
6 files changed, 446 insertions, 31 deletions
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:""` +} |
