[chore] Move local account settings to separate db table (#2770)

* [chore] Move local account settings to separate database model

* don't use separate settings_id
This commit is contained in:
tobi 2024-03-22 14:03:46 +01:00 committed by GitHub
commit 7f4a0a1aeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 525 additions and 191 deletions

View file

@ -117,4 +117,13 @@ type Account interface {
// GetInstanceAccount returns the instance account for the given domain.
// If domain is empty, this instance account will be returned.
GetInstanceAccount(ctx context.Context, domain string) (*gtsmodel.Account, error)
// Get local account settings with the given ID.
GetAccountSettings(ctx context.Context, id string) (*gtsmodel.AccountSettings, error)
// Store local account settings.
PutAccountSettings(ctx context.Context, settings *gtsmodel.AccountSettings) error
// Update local account settings.
UpdateAccountSettings(ctx context.Context, settings *gtsmodel.AccountSettings, columns ...string) error
}

View file

@ -338,6 +338,17 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
}
}
if account.IsLocal() && account.Settings == nil && !account.IsInstance() {
// Account settings not set, fetch from db.
account.Settings, err = a.state.DB.GetAccountSettings(
ctx, // these are already barebones
account.ID,
)
if err != nil {
errs.Appendf("error populating account settings: %w", err)
}
}
return errs.Combine()
}
@ -504,12 +515,22 @@ func (a *accountDB) SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachmen
}
func (a *accountDB) GetAccountCustomCSSByUsername(ctx context.Context, username string) (string, error) {
// Get local account.
account, err := a.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
return "", err
}
return account.CustomCSS, nil
// Ensure settings populated, in case
// barebones context was passed.
if account.Settings == nil {
account.Settings, err = a.GetAccountSettings(ctx, account.ID)
if err != nil {
return "", err
}
}
return account.Settings.CustomCSS, nil
}
func (a *accountDB) GetAccountsUsingEmoji(ctx context.Context, emojiID string) ([]*gtsmodel.Account, error) {
@ -780,3 +801,68 @@ func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string,
return a.state.DB.GetStatusesByIDs(ctx, statusIDs)
}
func (a *accountDB) GetAccountSettings(
ctx context.Context,
accountID string,
) (*gtsmodel.AccountSettings, error) {
// Fetch settings from db cache with loader callback.
return a.state.Caches.GTS.AccountSettings.LoadOne(
"AccountID",
func() (*gtsmodel.AccountSettings, error) {
// Not cached! Perform database query.
var settings gtsmodel.AccountSettings
if err := a.db.
NewSelect().
Model(&settings).
Where("? = ?", bun.Ident("account_settings.account_id"), accountID).
Scan(ctx); err != nil {
return nil, err
}
return &settings, nil
},
accountID,
)
}
func (a *accountDB) PutAccountSettings(
ctx context.Context,
settings *gtsmodel.AccountSettings,
) error {
return a.state.Caches.GTS.AccountSettings.Store(settings, func() error {
if _, err := a.db.
NewInsert().
Model(settings).
Exec(ctx); err != nil {
return err
}
return nil
})
}
func (a *accountDB) UpdateAccountSettings(
ctx context.Context,
settings *gtsmodel.AccountSettings,
columns ...string,
) error {
return a.state.Caches.GTS.AccountSettings.Store(settings, func() error {
settings.UpdatedAt = time.Now()
if len(columns) > 0 {
// If we're updating by column,
// ensure "updated_at" is included.
columns = append(columns, "updated_at")
}
if _, err := a.db.
NewUpdate().
Model(settings).
Column(columns...).
Where("? = ?", bun.Ident("account_settings.account_id"), settings.AccountID).
Exec(ctx); err != nil {
return err
}
return nil
})
}

View file

@ -216,6 +216,8 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
a2.AvatarMediaAttachment = nil
a1.Emojis = nil
a2.Emojis = nil
a1.Settings = nil
a2.Settings = nil
// Clear database-set fields.
a1.CreatedAt = time.Time{}
@ -439,15 +441,11 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
err = suite.db.Put(context.Background(), newAccount)
suite.NoError(err)
suite.Equal("en", newAccount.Language)
suite.WithinDuration(time.Now(), newAccount.CreatedAt, 30*time.Second)
suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second)
suite.True(*newAccount.Locked)
suite.False(*newAccount.Memorial)
suite.False(*newAccount.Bot)
suite.False(*newAccount.Discoverable)
suite.False(*newAccount.Sensitive)
suite.False(*newAccount.HideCollections)
}
func (suite *AccountTestSuite) TestGetAccountPinnedStatusesSomeResults() {

View file

@ -119,12 +119,21 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
return nil, err
}
settings := &gtsmodel.AccountSettings{
AccountID: accountID,
Reason: newSignup.Reason,
Privacy: gtsmodel.VisibilityDefault,
}
// Insert the settings!
if err := a.state.DB.PutAccountSettings(ctx, settings); err != nil {
return nil, err
}
account = &gtsmodel.Account{
ID: accountID,
Username: newSignup.Username,
DisplayName: newSignup.Username,
Reason: newSignup.Reason,
Privacy: gtsmodel.VisibilityDefault,
URI: uris.UserURI,
URL: uris.UserURL,
InboxURI: uris.InboxURI,
@ -136,6 +145,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
PrivateKey: privKey,
PublicKey: &privKey.PublicKey,
PublicKeyURI: uris.PublicKeyURI,
Settings: settings,
}
// Insert the new account!

View file

@ -85,19 +85,13 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
suite.Nil(a.Fields)
suite.Empty(a.Note)
suite.Empty(a.NoteRaw)
suite.False(*a.Memorial)
suite.Empty(a.AlsoKnownAsURIs)
suite.Empty(a.MovedToURI)
suite.False(*a.Bot)
suite.Empty(a.Reason)
// Locked is especially important, since it's a bool that defaults
// to true, which is why we use pointers for bools in the first place
suite.True(*a.Locked)
suite.False(*a.Discoverable)
suite.Empty(a.Privacy)
suite.False(*a.Sensitive)
suite.Equal("en", a.Language)
suite.Empty(a.StatusContentType)
suite.Equal(testAccount.URI, a.URI)
suite.Equal(testAccount.URL, a.URL)
suite.Zero(testAccount.FetchedAt)
@ -113,7 +107,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
suite.Zero(a.SensitizedAt)
suite.Zero(a.SilencedAt)
suite.Zero(a.SuspendedAt)
suite.False(*a.HideCollections)
suite.Empty(a.SuspensionOrigin)
}

View file

@ -0,0 +1,122 @@
// 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"
oldgtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20230328203024_migration_fix"
newgtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
log.Info(ctx, "migrating account settings to new table, please wait...")
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Columns we'll be moving
// to AccountSettings.
var columns = []string{
"reason",
"privacy",
"sensitive",
"language",
"status_content_type",
"custom_css",
"enable_rss",
"hide_collections",
}
// Create the new account settings table.
if _, err := tx.
NewCreateTable().
Model(&newgtsmodel.AccountSettings{}).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Select each local account.
accounts := []*oldgtsmodel.Account{}
if err := tx.
NewSelect().
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
Column("account.id").
Column(columns...).
Join(
"JOIN ? AS ? ON ? = ?",
bun.Ident("users"), bun.Ident("user"),
bun.Ident("user.account_id"), bun.Ident("account.id"),
).
Scan(ctx, &accounts); err != nil {
return err
}
// Create a settings entry for each existing account, taking
// values from the old account model (with sensible defaults).
for _, account := range accounts {
settings := &newgtsmodel.AccountSettings{
AccountID: account.ID,
CreatedAt: account.CreatedAt,
Reason: account.Reason,
Privacy: newgtsmodel.Visibility(account.Privacy),
Sensitive: util.Ptr(util.PtrValueOr(account.Sensitive, false)),
Language: account.Language,
StatusContentType: account.StatusContentType,
CustomCSS: account.CustomCSS,
EnableRSS: util.Ptr(util.PtrValueOr(account.EnableRSS, false)),
HideCollections: util.Ptr(util.PtrValueOr(account.HideCollections, false)),
}
// Insert the settings model.
if _, err := tx.
NewInsert().
Model(settings).
Exec(ctx); err != nil {
return err
}
}
// Drop now unused columns from accounts table.
for _, column := range columns {
if _, err := tx.
NewDropColumn().
Table("accounts").
Column(column).
Exec(ctx); err != nil {
return err
}
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}