From 7f4a0a1aeb8a294ee967c63d7a48446df013ec44 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:03:46 +0100 Subject: [PATCH] [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 --- .../api/activitypub/users/inboxpost_test.go | 7 - .../api/client/accounts/accountupdate_test.go | 2 +- .../api/client/accounts/accountverify_test.go | 2 +- .../api/client/statuses/statuscreate_test.go | 20 ++- .../wellknown/webfinger/webfingerget_test.go | 5 +- internal/cache/cache.go | 2 + internal/cache/db.go | 27 ++++ internal/cache/size.go | 22 +++- internal/config/config.go | 1 + internal/config/defaults.go | 1 + internal/config/helpers.gen.go | 25 ++++ internal/db/account.go | 9 ++ internal/db/bundb/account.go | 88 ++++++++++++- internal/db/bundb/account_test.go | 6 +- internal/db/bundb/admin.go | 14 +- internal/db/bundb/basic_test.go | 7 - .../20240318115336_account_settings.go | 122 ++++++++++++++++++ internal/federation/dereferencing/account.go | 3 - internal/gtsmodel/account.go | 71 +++++----- internal/gtsmodel/accountsettings.go | 35 +++++ internal/processing/account/delete.go | 10 -- internal/processing/account/delete_test.go | 5 - internal/processing/account/rss.go | 2 +- internal/processing/account/update.go | 26 ++-- internal/processing/account/update_test.go | 10 +- internal/processing/preferences.go | 6 +- internal/processing/status/create.go | 11 +- .../processing/workers/fromfediapi_test.go | 2 - internal/trans/import.go | 9 ++ internal/trans/import_test.go | 6 - internal/trans/model/account.go | 10 +- internal/typeutils/astointernal.go | 10 +- internal/typeutils/internaltofrontend.go | 31 +++-- test/envparsing.sh | 1 + testrig/db.go | 7 + testrig/testmodels.go | 101 ++++++++------- 36 files changed, 525 insertions(+), 191 deletions(-) create mode 100644 internal/db/bundb/migrations/20240318115336_account_settings.go create mode 100644 internal/gtsmodel/accountsettings.go diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go index 715231ecc..89e1cec41 100644 --- a/internal/api/activitypub/users/inboxpost_test.go +++ b/internal/api/activitypub/users/inboxpost_test.go @@ -395,12 +395,8 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { suite.EqualValues(requestingAccount.AlsoKnownAsURIs, dbUpdatedAccount.AlsoKnownAsURIs) suite.EqualValues(requestingAccount.MovedToURI, dbUpdatedAccount.MovedToURI) suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot) - suite.EqualValues(requestingAccount.Reason, dbUpdatedAccount.Reason) suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked) suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable) - suite.EqualValues(requestingAccount.Privacy, dbUpdatedAccount.Privacy) - suite.EqualValues(requestingAccount.Sensitive, dbUpdatedAccount.Sensitive) - suite.EqualValues(requestingAccount.Language, dbUpdatedAccount.Language) suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI) suite.EqualValues(requestingAccount.URL, dbUpdatedAccount.URL) suite.EqualValues(requestingAccount.InboxURI, dbUpdatedAccount.InboxURI) @@ -414,7 +410,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { suite.EqualValues(requestingAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt) suite.EqualValues(requestingAccount.SilencedAt, dbUpdatedAccount.SilencedAt) suite.EqualValues(requestingAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt) - suite.EqualValues(requestingAccount.HideCollections, dbUpdatedAccount.HideCollections) suite.EqualValues(requestingAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin) } @@ -464,9 +459,7 @@ func (suite *InboxPostTestSuite) TestPostDelete() { suite.Empty(dbAccount.AvatarRemoteURL) suite.Empty(dbAccount.HeaderMediaAttachmentID) suite.Empty(dbAccount.HeaderRemoteURL) - suite.Empty(dbAccount.Reason) suite.Empty(dbAccount.Fields) - suite.True(*dbAccount.HideCollections) suite.False(*dbAccount.Discoverable) suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second) suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) diff --git a/internal/api/client/accounts/accountupdate_test.go b/internal/api/client/accounts/accountupdate_test.go index 73e33390f..50443ceb6 100644 --- a/internal/api/client/accounts/accountupdate_test.go +++ b/internal/api/client/accounts/accountupdate_test.go @@ -481,7 +481,7 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceBadContentTypeFormDa if err != nil { suite.FailNow(err.Error()) } - suite.Equal(data["source[status_content_type]"][0], dbAccount.StatusContentType) + suite.Equal(data["source[status_content_type]"][0], dbAccount.Settings.StatusContentType) } func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusContentTypeBad() { diff --git a/internal/api/client/accounts/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go index 744488bf3..eccda8b2e 100644 --- a/internal/api/client/accounts/accountverify_test.go +++ b/internal/api/client/accounts/accountverify_test.go @@ -81,7 +81,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() { suite.Equal(2, apimodelAccount.FollowingCount) suite.Equal(7, apimodelAccount.StatusesCount) suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy) - suite.Equal(testAccount.Language, apimodelAccount.Source.Language) + suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language) suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note) } diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go index ab7c67abf..b49e72ead 100644 --- a/internal/api/client/statuses/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -103,16 +103,22 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { } func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { - // set default post language of account 1 to markdown - testAccount := suite.testAccounts["local_account_1"] - testAccount.StatusContentType = "text/markdown" - a := testAccount + // Copy zork. + testAccount := >smodel.Account{} + *testAccount = *suite.testAccounts["local_account_1"] - err := suite.db.UpdateAccount(context.Background(), a) + // Copy zork's settings. + settings := >smodel.AccountSettings{} + *settings = *suite.testAccounts["local_account_1"].Settings + testAccount.Settings = settings + + // set default post language of zork to markdown + testAccount.Settings.StatusContentType = "text/markdown" + err := suite.db.UpdateAccountSettings(context.Background(), testAccount.Settings) if err != nil { suite.FailNow(err.Error()) } - suite.Equal(a.StatusContentType, "text/markdown") + suite.Equal(testAccount.Settings.StatusContentType, "text/markdown") t := suite.testTokens["local_account_1"] oauthToken := oauth.DBTokenToToken(t) @@ -122,7 +128,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) - ctx.Set(oauth.SessionAuthorizedAccount, a) + ctx.Set(oauth.SessionAuthorizedAccount, testAccount) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) ctx.Request.Header.Set("accept", "application/json") diff --git a/internal/api/wellknown/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go index c9bd088be..84562187d 100644 --- a/internal/api/wellknown/webfinger/webfingerget_test.go +++ b/internal/api/wellknown/webfinger/webfingerget_test.go @@ -100,7 +100,6 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom targetAccount := >smodel.Account{ ID: "01FG1K8EA7SYHEC7V6XKVNC4ZA", Username: "new_account_domain_user", - Privacy: gtsmodel.VisibilityDefault, URI: "http://" + host + "/users/new_account_domain_user", URL: "http://" + host + "/@new_account_domain_user", InboxURI: "http://" + host + "/users/new_account_domain_user/inbox", @@ -118,6 +117,10 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom suite.FailNow(err.Error()) } + if err := suite.db.PutAccountSettings(context.Background(), >smodel.AccountSettings{AccountID: targetAccount.ID}); err != nil { + suite.FailNow(err.Error()) + } + return targetAccount } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index e2fe43a1f..fd715b8e6 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -53,6 +53,7 @@ func (c *Caches) Init() { c.initAccount() c.initAccountCounts() c.initAccountNote() + c.initAccountSettings() c.initApplication() c.initBlock() c.initBlockIDs() @@ -119,6 +120,7 @@ func (c *Caches) Stop() { func (c *Caches) Sweep(threshold float64) { c.GTS.Account.Trim(threshold) c.GTS.AccountNote.Trim(threshold) + c.GTS.AccountSettings.Trim(threshold) c.GTS.Block.Trim(threshold) c.GTS.BlockIDs.Trim(threshold) c.GTS.Emoji.Trim(threshold) diff --git a/internal/cache/db.go b/internal/cache/db.go index 00dfe204a..ff38c1d93 100644 --- a/internal/cache/db.go +++ b/internal/cache/db.go @@ -43,6 +43,9 @@ type GTSCaches struct { Pinned int }] + // AccountSettings provides access to the gtsmodel AccountSettings database cache. + AccountSettings structr.Cache[*gtsmodel.AccountSettings] + // Application provides access to the gtsmodel Application database cache. Application structr.Cache[*gtsmodel.Application] @@ -190,6 +193,7 @@ func (c *Caches) initAccount() { a2.Emojis = nil a2.AlsoKnownAs = nil a2.Move = nil + a2.Settings = nil return a2 } @@ -262,6 +266,29 @@ func (c *Caches) initAccountNote() { }) } +func (c *Caches) initAccountSettings() { + // Calculate maximum cache size. + cap := calculateResultCacheMax( + sizeofAccountSettings(), // model in-mem size. + config.GetCacheAccountSettingsMemRatio(), + ) + + log.Infof(nil, "cache size = %d", cap) + + c.GTS.AccountSettings.Init(structr.Config[*gtsmodel.AccountSettings]{ + Indices: []structr.IndexConfig{ + {Fields: "AccountID"}, + }, + MaxSize: cap, + IgnoreErr: ignoreErrors, + CopyValue: func(s1 *gtsmodel.AccountSettings) *gtsmodel.AccountSettings { + s2 := new(gtsmodel.AccountSettings) + *s2 = *s1 + return s2 + }, + }) +} + func (c *Caches) initApplication() { // Calculate maximum cache size. cap := calculateResultCacheMax( diff --git a/internal/cache/size.go b/internal/cache/size.go index b1c431c55..080fefea3 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -28,6 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/util" ) const ( @@ -219,9 +220,6 @@ func sizeofAccount() uintptr { Bot: func() *bool { ok := true; return &ok }(), Locked: func() *bool { ok := true; return &ok }(), Discoverable: func() *bool { ok := false; return &ok }(), - Privacy: gtsmodel.VisibilityFollowersOnly, - Sensitive: func() *bool { ok := true; return &ok }(), - Language: "fr", URI: exampleURI, URL: exampleURI, InboxURI: exampleURI, @@ -236,9 +234,7 @@ func sizeofAccount() uintptr { SensitizedAt: exampleTime, SilencedAt: exampleTime, SuspendedAt: exampleTime, - HideCollections: func() *bool { ok := true; return &ok }(), SuspensionOrigin: exampleID, - EnableRSS: func() *bool { ok := true; return &ok }(), })) } @@ -251,6 +247,22 @@ func sizeofAccountNote() uintptr { })) } +func sizeofAccountSettings() uintptr { + return uintptr(size.Of(>smodel.AccountSettings{ + AccountID: exampleID, + CreatedAt: exampleTime, + UpdatedAt: exampleTime, + Reason: exampleText, + Privacy: gtsmodel.VisibilityFollowersOnly, + Sensitive: util.Ptr(true), + Language: "fr", + StatusContentType: "text/plain", + CustomCSS: exampleText, + EnableRSS: util.Ptr(true), + HideCollections: util.Ptr(false), + })) +} + func sizeofApplication() uintptr { return uintptr(size.Of(>smodel.Application{ ID: exampleID, diff --git a/internal/config/config.go b/internal/config/config.go index f4ea64f93..a6d27217f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -195,6 +195,7 @@ type CacheConfiguration struct { MemoryTarget bytesize.Size `name:"memory-target"` AccountMemRatio float64 `name:"account-mem-ratio"` AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` + AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"` ApplicationMemRatio float64 `name:"application-mem-ratio"` BlockMemRatio float64 `name:"block-mem-ratio"` BlockIDsMemRatio float64 `name:"block-mem-ratio"` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 6ca508d5a..99a2e24cb 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -159,6 +159,7 @@ // be able to make some more sense :D AccountMemRatio: 5, AccountNoteMemRatio: 1, + AccountSettingsMemRatio: 0.1, ApplicationMemRatio: 0.1, BlockMemRatio: 2, BlockIDsMemRatio: 3, diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index 5f65a6e28..39c163d20 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -2825,6 +2825,31 @@ func GetCacheAccountNoteMemRatio() float64 { return global.GetCacheAccountNoteMe // SetCacheAccountNoteMemRatio safely sets the value for global configuration 'Cache.AccountNoteMemRatio' field func SetCacheAccountNoteMemRatio(v float64) { global.SetCacheAccountNoteMemRatio(v) } +// GetCacheAccountSettingsMemRatio safely fetches the Configuration value for state's 'Cache.AccountSettingsMemRatio' field +func (st *ConfigState) GetCacheAccountSettingsMemRatio() (v float64) { + st.mutex.RLock() + v = st.config.Cache.AccountSettingsMemRatio + st.mutex.RUnlock() + return +} + +// SetCacheAccountSettingsMemRatio safely sets the Configuration value for state's 'Cache.AccountSettingsMemRatio' field +func (st *ConfigState) SetCacheAccountSettingsMemRatio(v float64) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.Cache.AccountSettingsMemRatio = v + st.reloadToViper() +} + +// CacheAccountSettingsMemRatioFlag returns the flag name for the 'Cache.AccountSettingsMemRatio' field +func CacheAccountSettingsMemRatioFlag() string { return "cache-account-settings-mem-ratio" } + +// GetCacheAccountSettingsMemRatio safely fetches the value for global configuration 'Cache.AccountSettingsMemRatio' field +func GetCacheAccountSettingsMemRatio() float64 { return global.GetCacheAccountSettingsMemRatio() } + +// SetCacheAccountSettingsMemRatio safely sets the value for global configuration 'Cache.AccountSettingsMemRatio' field +func SetCacheAccountSettingsMemRatio(v float64) { global.SetCacheAccountSettingsMemRatio(v) } + // GetCacheApplicationMemRatio safely fetches the Configuration value for state's 'Cache.ApplicationMemRatio' field func (st *ConfigState) GetCacheApplicationMemRatio() (v float64) { st.mutex.RLock() diff --git a/internal/db/account.go b/internal/db/account.go index 505ca4004..3de72c5a8 100644 --- a/internal/db/account.go +++ b/internal/db/account.go @@ -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 } diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 5b1dab143..870c2ff55 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -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 + }) +} diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index 268c25c59..21e04dedc 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -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() { diff --git a/internal/db/bundb/admin.go b/internal/db/bundb/admin.go index 70ae68026..832db1d8f 100644 --- a/internal/db/bundb/admin.go +++ b/internal/db/bundb/admin.go @@ -119,12 +119,21 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) ( return nil, err } + settings := >smodel.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 = >smodel.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! diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go index 5d5c1c2b9..6892291d2 100644 --- a/internal/db/bundb/basic_test.go +++ b/internal/db/bundb/basic_test.go @@ -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) } diff --git a/internal/db/bundb/migrations/20240318115336_account_settings.go b/internal/db/bundb/migrations/20240318115336_account_settings.go new file mode 100644 index 000000000..90d3ff420 --- /dev/null +++ b/internal/db/bundb/migrations/20240318115336_account_settings.go @@ -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 . + +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) + } +} diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index 5e81fb445..305b3f05c 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -743,9 +743,6 @@ func (d *Dereferencer) enrichAccount( // Set time of update from the last-fetched date. latestAcc.UpdatedAt = latestAcc.FetchedAt - // Carry over existing account language. - latestAcc.Language = account.Language - // This is an existing account, update the model in the database. if err := d.state.DB.UpdateAccount(ctx, latestAcc); err != nil { return nil, nil, gtserror.Newf("error updating database: %w", err) diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 643dd62b8..2ac107e56 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -48,45 +48,38 @@ type Account struct { DisplayName string `bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this account's bio, display name, etc Emojis []*Emoji `bun:"attached_emojis,m2m:account_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation - Fields []*Field // A slice of of fields that this account has added to their profile. - FieldsRaw []*Field // The raw (unparsed) content of fields that this account has added to their profile, without conversion to HTML, only available when requester = target - Note string `bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves) - NoteRaw string `bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target - Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away? - AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs. - AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db). - MovedToURI string `bun:",nullzero"` // This account has (or claims to have) moved to this account URI. Even if this field is set the move may not yet have been processed. Check `move` for this. - MovedTo *Account `bun:"-"` // This account has moved to this account (field not stored in the db). - MoveID string `bun:"type:CHAR(26),nullzero"` // ID of a Move in the database for this account. Only set if we received or created a Move activity for which this account URI was the origin. - Move *Move `bun:"-"` // Move corresponding to MoveID, if set. - Bot *bool `bun:",default:false"` // Does this account identify itself as a bot? - Reason string `bun:""` // What reason was given for signing up when this account was created? - Locked *bool `bun:",default:true"` // Does this account need an approval for new followers? - Discoverable *bool `bun:",default:false"` // Should this account be shown in the instance's profile directory? - Privacy Visibility `bun:",nullzero"` // Default post privacy for this account - Sensitive *bool `bun:",default:false"` // Set posts from this account to sensitive by default? - Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in? - StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts). - CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses. - URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI for this account. - URL string `bun:",nullzero,unique"` // Web URL for this account's profile - InboxURI string `bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to - SharedInboxURI *string `bun:""` // Address of this account's ActivityPub sharedInbox. Gotcha warning: this is a string pointer because it has three possible states: 1. We don't know yet if the account has a shared inbox -- null. 2. We know it doesn't have a shared inbox -- empty string. 3. We know it does have a shared inbox -- url string. - OutboxURI string `bun:",nullzero,unique"` // Address of this account's activitypub outbox - FollowingURI string `bun:",nullzero,unique"` // URI for getting the following list of this account - FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account - FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account - ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account? - PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts - PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for authorizing signed activitypub requests, will be defined for both local and remote accounts - PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key - PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // PublicKey will expire/has expired at given time, and should be fetched again as appropriate. Only ever set for remote accounts. - SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive? - SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)? - SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) - HideCollections *bool `bun:",default:false"` // Hide this account's collections - SuspensionOrigin string `bun:"type:CHAR(26),nullzero"` // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID - EnableRSS *bool `bun:",default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed + Fields []*Field `bun:""` // A slice of of fields that this account has added to their profile. + FieldsRaw []*Field `bun:""` // The raw (unparsed) content of fields that this account has added to their profile, without conversion to HTML, only available when requester = target + Note string `bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves) + NoteRaw string `bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target + Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away? + AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs. + AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db). + MovedToURI string `bun:",nullzero"` // This account has (or claims to have) moved to this account URI. Even if this field is set the move may not yet have been processed. Check `move` for this. + MovedTo *Account `bun:"-"` // This account has moved to this account (field not stored in the db). + MoveID string `bun:"type:CHAR(26),nullzero"` // ID of a Move in the database for this account. Only set if we received or created a Move activity for which this account URI was the origin. + Move *Move `bun:"-"` // Move corresponding to MoveID, if set. + Bot *bool `bun:",default:false"` // Does this account identify itself as a bot? + Locked *bool `bun:",default:true"` // Does this account need an approval for new followers? + Discoverable *bool `bun:",default:false"` // Should this account be shown in the instance's profile directory? + URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI for this account. + URL string `bun:",nullzero,unique"` // Web URL for this account's profile + InboxURI string `bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to + SharedInboxURI *string `bun:""` // Address of this account's ActivityPub sharedInbox. Gotcha warning: this is a string pointer because it has three possible states: 1. We don't know yet if the account has a shared inbox -- null. 2. We know it doesn't have a shared inbox -- empty string. 3. We know it does have a shared inbox -- url string. + OutboxURI string `bun:",nullzero,unique"` // Address of this account's activitypub outbox + FollowingURI string `bun:",nullzero,unique"` // URI for getting the following list of this account + FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account + FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account + ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account? + PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts + PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for authorizing signed activitypub requests, will be defined for both local and remote accounts + PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key + PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // PublicKey will expire/has expired at given time, and should be fetched again as appropriate. Only ever set for remote accounts. + SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive? + SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)? + SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) + SuspensionOrigin string `bun:"type:CHAR(26),nullzero"` // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID + Settings *AccountSettings `bun:"-"` // gtsmodel.AccountSettings for this account. } // IsLocal returns whether account is a local user account. diff --git a/internal/gtsmodel/accountsettings.go b/internal/gtsmodel/accountsettings.go new file mode 100644 index 000000000..cb5411050 --- /dev/null +++ b/internal/gtsmodel/accountsettings.go @@ -0,0 +1,35 @@ +// 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 . + +package gtsmodel + +import "time" + +// AccountSettings models settings / preferences for a local, non-instance account. +type AccountSettings struct { + AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings. + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created. + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated. + Reason string `bun:",nullzero"` // What reason was given for signing up when this account was created? + Privacy Visibility `bun:",nullzero"` // Default post privacy for this account + Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default? + Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in? + StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts). + 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. +} diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index ff68a4638..2ae00194e 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -518,14 +518,9 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string { account.Memorial = util.Ptr(false) account.AlsoKnownAsURIs = nil account.MovedToURI = "" - account.Reason = "" account.Discoverable = util.Ptr(false) - account.StatusContentType = "" - account.CustomCSS = "" account.SuspendedAt = now account.SuspensionOrigin = origin - account.HideCollections = util.Ptr(true) - account.EnableRSS = util.Ptr(false) return []string{ "fetched_at", @@ -541,14 +536,9 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string { "memorial", "also_known_as_uris", "moved_to_uri", - "reason", "discoverable", - "status_content_type", - "custom_css", "suspended_at", "suspension_origin", - "hide_collections", - "enable_rss", } } diff --git a/internal/processing/account/delete_test.go b/internal/processing/account/delete_test.go index 95df3cec5..de7c8e08c 100644 --- a/internal/processing/account/delete_test.go +++ b/internal/processing/account/delete_test.go @@ -66,14 +66,9 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() { suite.Zero(updatedAccount.NoteRaw) suite.False(*updatedAccount.Memorial) suite.Empty(updatedAccount.AlsoKnownAsURIs) - suite.Zero(updatedAccount.Reason) suite.False(*updatedAccount.Discoverable) - suite.Zero(updatedAccount.StatusContentType) - suite.Zero(updatedAccount.CustomCSS) suite.WithinDuration(time.Now(), updatedAccount.SuspendedAt, 1*time.Minute) suite.Equal(suspensionOrigin, updatedAccount.SuspensionOrigin) - suite.True(*updatedAccount.HideCollections) - suite.False(*updatedAccount.EnableRSS) updatedUser, err := suite.db.GetUserByAccountID(ctx, testAccount.ID) if err != nil { diff --git a/internal/processing/account/rss.go b/internal/processing/account/rss.go index df49af21f..f2c6cba5e 100644 --- a/internal/processing/account/rss.go +++ b/internal/processing/account/rss.go @@ -64,7 +64,7 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string) } // Ensure account has rss feed enabled. - if !*account.EnableRSS { + if !*account.Settings.EnableRSS { err = gtserror.New("account RSS feed not enabled") return nil, never, gtserror.NewErrorNotFound(err) } diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 5dc93fa1d..7b5561138 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -47,6 +47,11 @@ func (p *Processor) selectNoteFormatter(contentType string) text.FormatFunc { // Update processes the update of an account with the given form. func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { + // Ensure account populated; we'll need settings. + if err := p.state.DB.PopulateAccount(ctx, account); err != nil { + log.Errorf(ctx, "error(s) populating account, will continue: %s", err) + } + if form.Discoverable != nil { account.Discoverable = form.Discoverable } @@ -146,7 +151,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form } // Format + set note according to user prefs. - f := p.selectNoteFormatter(account.StatusContentType) + f := p.selectNoteFormatter(account.Settings.StatusContentType) formatNoteResult := f(ctx, p.parseMention, account.ID, "", account.NoteRaw) account.Note = formatNoteResult.HTML @@ -227,11 +232,11 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form if err != nil { return nil, gtserror.NewErrorBadRequest(err) } - account.Language = language + account.Settings.Language = language } if form.Source.Sensitive != nil { - account.Sensitive = form.Source.Sensitive + account.Settings.Sensitive = form.Source.Sensitive } if form.Source.Privacy != nil { @@ -239,7 +244,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form return nil, gtserror.NewErrorBadRequest(err) } privacy := typeutils.APIVisToVis(apimodel.Visibility(*form.Source.Privacy)) - account.Privacy = privacy + account.Settings.Privacy = privacy } if form.Source.StatusContentType != nil { @@ -247,7 +252,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - account.StatusContentType = *form.Source.StatusContentType + account.Settings.StatusContentType = *form.Source.StatusContentType } } @@ -256,18 +261,21 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form if err := validate.CustomCSS(customCSS); err != nil { return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - account.CustomCSS = text.SanitizeToPlaintext(customCSS) + account.Settings.CustomCSS = text.SanitizeToPlaintext(customCSS) } if form.EnableRSS != nil { - account.EnableRSS = form.EnableRSS + account.Settings.EnableRSS = form.EnableRSS } - err := p.state.DB.UpdateAccount(ctx, account) - if err != nil { + if err := p.state.DB.UpdateAccount(ctx, account); err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err)) } + if err := p.state.DB.UpdateAccountSettings(ctx, account.Settings); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account settings %s: %s", account.ID, err)) + } + p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ APObjectType: ap.ObjectProfile, APActivityType: ap.ActivityUpdate, diff --git a/internal/processing/account/update_test.go b/internal/processing/account/update_test.go index 87b4ebd50..76ad3abe8 100644 --- a/internal/processing/account/update_test.go +++ b/internal/processing/account/update_test.go @@ -126,9 +126,15 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() { } func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() { + // Copy zork. testAccount := >smodel.Account{} *testAccount = *suite.testAccounts["local_account_1"] + // Copy zork's settings. + settings := >smodel.AccountSettings{} + *settings = *suite.testAccounts["local_account_1"].Settings + testAccount.Settings = settings + var ( ctx = context.Background() note = "*hello* ~~here~~ i am!" @@ -136,8 +142,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() { ) // Set status content type of account 1 to markdown for this test. - testAccount.StatusContentType = "text/markdown" - if err := suite.db.UpdateAccount(ctx, testAccount, "status_content_type"); err != nil { + testAccount.Settings.StatusContentType = "text/markdown" + if err := suite.db.UpdateAccountSettings(ctx, testAccount.Settings, "status_content_type"); err != nil { suite.FailNow(err.Error()) } diff --git a/internal/processing/preferences.go b/internal/processing/preferences.go index 90fc86430..0a5f566ae 100644 --- a/internal/processing/preferences.go +++ b/internal/processing/preferences.go @@ -32,9 +32,9 @@ func (p *Processor) PreferencesGet(ctx context.Context, accountID string) (*apim } return &apimodel.Preferences{ - PostingDefaultVisibility: mastoPrefVisibility(act.Privacy), - PostingDefaultSensitive: *act.Sensitive, - PostingDefaultLanguage: act.Language, + PostingDefaultVisibility: mastoPrefVisibility(act.Settings.Privacy), + PostingDefaultSensitive: *act.Settings.Sensitive, + PostingDefaultLanguage: act.Settings.Language, // The Reading* preferences don't appear to actually be settable by the // client, so forcing some sensible defaults here ReadingExpandMedia: "default", diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go index 01ded74bd..d758fc0fb 100644 --- a/internal/processing/status/create.go +++ b/internal/processing/status/create.go @@ -50,6 +50,11 @@ func (p *Processor) Create( *apimodel.Status, gtserror.WithCode, ) { + // Ensure account populated; we'll need settings. + if err := p.state.DB.PopulateAccount(ctx, requester); err != nil { + log.Errorf(ctx, "error(s) populating account, will continue: %s", err) + } + // Generate new ID for status. statusID := id.NewULID() @@ -112,11 +117,11 @@ func (p *Processor) Create( return nil, errWithCode } - if err := processVisibility(form, requester.Privacy, status); err != nil { + if err := processVisibility(form, requester.Settings.Privacy, status); err != nil { return nil, gtserror.NewErrorInternalError(err) } - if err := processLanguage(form, requester.Language, status); err != nil { + if err := processLanguage(form, requester.Settings.Language, status); err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -369,7 +374,7 @@ func processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLang func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, status *gtsmodel.Status) error { if form.ContentType == "" { // If content type wasn't specified, use the author's preferred content-type. - contentType := apimodel.StatusContentType(status.Account.StatusContentType) + contentType := apimodel.StatusContentType(status.Account.Settings.StatusContentType) form.ContentType = contentType } diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go index b7466ec73..1dbefca84 100644 --- a/internal/processing/workers/fromfediapi_test.go +++ b/internal/processing/workers/fromfediapi_test.go @@ -362,9 +362,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() { suite.Empty(dbAccount.AvatarRemoteURL) suite.Empty(dbAccount.HeaderMediaAttachmentID) suite.Empty(dbAccount.HeaderRemoteURL) - suite.Empty(dbAccount.Reason) suite.Empty(dbAccount.Fields) - suite.True(*dbAccount.HideCollections) suite.False(*dbAccount.Discoverable) suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second) suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) diff --git a/internal/trans/import.go b/internal/trans/import.go index 0fd539114..c77b439f5 100644 --- a/internal/trans/import.go +++ b/internal/trans/import.go @@ -25,6 +25,7 @@ "io" "os" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/log" transmodel "github.com/superseriousbusiness/gotosocial/internal/trans/model" ) @@ -73,6 +74,14 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error if err := i.putInDB(ctx, account); err != nil { return fmt.Errorf("inputEntry: error adding account to database: %s", err) } + if account.Domain == "" && account.Username != config.GetHost() { + // Local, non-instance account. + // Insert barebones settings model. + settings := &transmodel.AccountSettings{AccountID: account.ID} + if err := i.putInDB(ctx, settings); err != nil { + return fmt.Errorf("inputEntry: error adding account settings to database: %s", err) + } + } log.Infof(ctx, "added account with id %s", account.ID) return nil case transmodel.TransBlock: diff --git a/internal/trans/import_test.go b/internal/trans/import_test.go index ac70efd29..5177ec45b 100644 --- a/internal/trans/import_test.go +++ b/internal/trans/import_test.go @@ -106,11 +106,6 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() { suite.Equal(testAccountBefore.Memorial, testAccountAfter.Memorial) suite.Equal(testAccountBefore.Bot, testAccountAfter.Bot) suite.Equal(testAccountBefore.Locked, testAccountAfter.Locked) - suite.Equal(testAccountBefore.Reason, testAccountAfter.Reason) - suite.Equal(testAccountBefore.Privacy, testAccountAfter.Privacy) - suite.Equal(testAccountBefore.Sensitive, testAccountAfter.Sensitive) - suite.Equal(testAccountBefore.Language, testAccountAfter.Language) - suite.Equal(testAccountBefore.StatusContentType, testAccountAfter.StatusContentType) suite.Equal(testAccountBefore.URI, testAccountAfter.URI) suite.Equal(testAccountBefore.URL, testAccountAfter.URL) suite.Equal(testAccountBefore.InboxURI, testAccountAfter.InboxURI) @@ -123,7 +118,6 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() { suite.Equal(testAccountBefore.PublicKey, testAccountAfter.PublicKey) suite.Equal(testAccountBefore.PublicKeyURI, testAccountAfter.PublicKeyURI) suite.Equal(testAccountBefore.SuspendedAt, testAccountAfter.SuspendedAt) - suite.Equal(testAccountBefore.HideCollections, testAccountAfter.HideCollections) suite.Equal(testAccountBefore.SuspensionOrigin, testAccountAfter.SuspensionOrigin) } diff --git a/internal/trans/model/account.go b/internal/trans/model/account.go index 73695f7be..097dea3a3 100644 --- a/internal/trans/model/account.go +++ b/internal/trans/model/account.go @@ -36,13 +36,8 @@ type Account struct { NoteRaw string `json:"noteRaw,omitempty" bun:",nullzero"` Memorial *bool `json:"memorial"` Bot *bool `json:"bot"` - Reason string `json:"reason,omitempty" bun:",nullzero"` Locked *bool `json:"locked"` Discoverable *bool `json:"discoverable"` - Privacy string `json:"privacy,omitempty" bun:",nullzero"` - Sensitive *bool `json:"sensitive"` - Language string `json:"language,omitempty" bun:",nullzero"` - StatusContentType string `json:"statusContentType,omitempty" bun:",nullzero"` URI string `json:"uri" bun:",nullzero"` URL string `json:"url" bun:",nullzero"` InboxURI string `json:"inboxURI" bun:",nullzero"` @@ -59,6 +54,9 @@ type Account struct { SensitizedAt *time.Time `json:"sensitizedAt,omitempty" bun:",nullzero"` SilencedAt *time.Time `json:"silencedAt,omitempty" bun:",nullzero"` SuspendedAt *time.Time `json:"suspendedAt,omitempty" bun:",nullzero"` - HideCollections *bool `json:"hideCollections"` SuspensionOrigin string `json:"suspensionOrigin,omitempty" bun:",nullzero"` } + +type AccountSettings struct { + AccountID string +} diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index b262030de..b5e713554 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -130,13 +130,8 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a // Extract account note (bio / summary). acct.Note = ap.ExtractSummary(accountable) - // Assume: - // - memorial (TODO) - // - sensitive (TODO) - // - hide collections (TODO) + // Assume not memorial (todo) acct.Memorial = util.Ptr(false) - acct.Sensitive = util.Ptr(false) - acct.HideCollections = util.Ptr(false) // Extract 'manuallyApprovesFollowers' aka locked account (default = true). manuallyApprovesFollowers := ap.GetManuallyApprovesFollowers(accountable) @@ -146,9 +141,6 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a discoverable := ap.GetDiscoverable(accountable) acct.Discoverable = &discoverable - // Assume not an RSS feed. - acct.EnableRSS = util.Ptr(false) - // Extract the URL property. urls := ap.GetURL(accountable) if len(urls) == 0 { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index df4598deb..0ff3c2268 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -78,14 +78,14 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode } statusContentType := string(apimodel.StatusContentTypeDefault) - if a.StatusContentType != "" { - statusContentType = a.StatusContentType + if a.Settings.StatusContentType != "" { + statusContentType = a.Settings.StatusContentType } apiAccount.Source = &apimodel.Source{ - Privacy: c.VisToAPIVis(ctx, a.Privacy), - Sensitive: *a.Sensitive, - Language: a.Language, + Privacy: c.VisToAPIVis(ctx, a.Settings.Privacy), + Sensitive: *a.Settings.Sensitive, + Language: a.Settings.Language, StatusContentType: statusContentType, Note: a.NoteRaw, Fields: c.fieldsToAPIFields(a.FieldsRaw), @@ -170,10 +170,13 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A // Bits that vary between remote + local accounts: // - Account (acct) string. // - Role. + // - Settings things (enableRSS, customCSS). var ( - acct string - role *apimodel.AccountRole + acct string + role *apimodel.AccountRole + enableRSS bool + customCSS string ) if a.IsRemote() { @@ -203,6 +206,9 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A default: role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser} } + + enableRSS = *a.Settings.EnableRSS + customCSS = a.Settings.CustomCSS } acct = a.Username // omit domain @@ -239,7 +245,6 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A locked = boolPtrDef("locked", a.Locked, true) discoverable = boolPtrDef("discoverable", a.Discoverable, false) bot = boolPtrDef("bot", a.Bot, false) - enableRSS = boolPtrDef("enableRSS", a.EnableRSS, false) ) // Remaining properties are simple and @@ -267,7 +272,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A Emojis: apiEmojis, Fields: fields, Suspended: !a.SuspendedAt.IsZero(), - CustomCSS: a.CustomCSS, + CustomCSS: customCSS, EnableRSS: enableRSS, Role: role, Moved: moved, @@ -376,6 +381,10 @@ func (c *Converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac createdByApplicationID string ) + if err := c.state.DB.PopulateAccount(ctx, a); err != nil { + log.Errorf(ctx, "error(s) populating account, will continue: %s", err) + } + if a.IsRemote() { // Domain may be in Punycode, // de-punify it just in case. @@ -404,8 +413,8 @@ func (c *Converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac } locale = user.Locale - if user.Account.Reason != "" { - inviteRequest = &user.Account.Reason + if a.Settings.Reason != "" { + inviteRequest = &a.Settings.Reason } if *user.Admin { diff --git a/test/envparsing.sh b/test/envparsing.sh index 72b7caa1d..ed136064d 100755 --- a/test/envparsing.sh +++ b/test/envparsing.sh @@ -26,6 +26,7 @@ EXPECT=$(cat << "EOF" "cache": { "account-mem-ratio": 5, "account-note-mem-ratio": 1, + "account-settings-mem-ratio": 0.1, "application-mem-ratio": 0.1, "block-mem-ratio": 3, "boost-of-ids-mem-ratio": 3, diff --git a/testrig/db.go b/testrig/db.go index a83d93b16..83bc46ec8 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -70,6 +70,7 @@ >smodel.Report{}, >smodel.Rule{}, >smodel.AccountNote{}, + >smodel.AccountSettings{}, } // NewTestDB returns a new initialized, empty database for testing. @@ -206,6 +207,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { } } + for _, v := range NewTestAccountSettings() { + if err := db.Put(ctx, v); err != nil { + log.Panic(nil, err) + } + } + for _, v := range NewTestAttachments() { if err := db.Put(ctx, v); err != nil { log.Panic(nil, err) diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 929317904..9d014bbca 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -286,6 +286,8 @@ func NewTestUsers() map[string]*gtsmodel.User { // NewTestAccounts returns a map of accounts keyed by what type of account they are. func NewTestAccounts() map[string]*gtsmodel.Account { + settings := NewTestAccountSettings() + accounts := map[string]*gtsmodel.Account{ "instance_account": { ID: "01AY6P665V14JJR0AFVRT7311Y", @@ -301,12 +303,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { CreatedAt: TimeMustParse("2020-05-17T13:10:59Z"), UpdatedAt: TimeMustParse("2020-05-17T13:10:59Z"), Bot: util.Ptr(false), - Reason: "", Locked: util.Ptr(false), Discoverable: util.Ptr(true), - Privacy: gtsmodel.VisibilityPublic, - Sensitive: util.Ptr(false), - Language: "en", URI: "http://localhost:8080/users/localhost:8080", URL: "http://localhost:8080/@localhost:8080", PublicKeyURI: "http://localhost:8080/users/localhost:8080#main-key", @@ -322,9 +320,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", - EnableRSS: util.Ptr(false), }, "unconfirmed_account": { ID: "01F8MH0BBE4FHXPH513MBVFHB0", @@ -339,12 +335,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), Bot: util.Ptr(false), - Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.", Locked: util.Ptr(false), Discoverable: util.Ptr(false), - Privacy: gtsmodel.VisibilityPublic, - Sensitive: util.Ptr(false), - Language: "en", URI: "http://localhost:8080/users/weed_lord420", URL: "http://localhost:8080/@weed_lord420", FetchedAt: time.Time{}, @@ -360,9 +352,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", - EnableRSS: util.Ptr(false), + Settings: settings["unconfirmed_account"], }, "admin_account": { ID: "01F8MH17FWEB39HZJ76B6VXSKF", @@ -378,12 +369,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"), UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"), Bot: util.Ptr(false), - Reason: "", Locked: util.Ptr(false), Discoverable: util.Ptr(true), - Privacy: gtsmodel.VisibilityPublic, - Sensitive: util.Ptr(false), - Language: "en", URI: "http://localhost:8080/users/admin", URL: "http://localhost:8080/@admin", PublicKeyURI: "http://localhost:8080/users/admin#main-key", @@ -399,9 +386,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", - EnableRSS: util.Ptr(true), + Settings: settings["admin_account"], }, "local_account_1": { ID: "01F8MH1H7YV1Z7D2C8K2730QBF", @@ -417,12 +403,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"), UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"), Bot: util.Ptr(false), - Reason: "I wanna be on this damned webbed site so bad! Please! Wow", Locked: util.Ptr(false), Discoverable: util.Ptr(true), - Privacy: gtsmodel.VisibilityPublic, - Sensitive: util.Ptr(false), - Language: "en", URI: "http://localhost:8080/users/the_mighty_zork", URL: "http://localhost:8080/@the_mighty_zork", FetchedAt: time.Time{}, @@ -438,9 +420,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", - EnableRSS: util.Ptr(true), + Settings: settings["local_account_1"], }, "local_account_2": { ID: "01F8MH5NBDF2MV7CTC4Q5128HF", @@ -475,12 +456,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"), UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), Bot: util.Ptr(false), - Reason: "", Locked: util.Ptr(true), Discoverable: util.Ptr(false), - Privacy: gtsmodel.VisibilityFollowersOnly, - Sensitive: util.Ptr(true), - Language: "fr", URI: "http://localhost:8080/users/1happyturtle", URL: "http://localhost:8080/@1happyturtle", FetchedAt: time.Time{}, @@ -496,9 +473,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", - EnableRSS: util.Ptr(false), + Settings: settings["local_account_2"], }, "remote_account_1": { ID: "01F8MH5ZK5VRH73AKHQM6Y9VNX", @@ -514,8 +490,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account { Bot: util.Ptr(false), Locked: util.Ptr(false), Discoverable: util.Ptr(true), - Sensitive: util.Ptr(false), - Language: "en", URI: "http://fossbros-anonymous.io/users/foss_satan", URL: "http://fossbros-anonymous.io/@foss_satan", FetchedAt: time.Time{}, @@ -532,9 +506,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", - EnableRSS: util.Ptr(false), }, "remote_account_2": { ID: "01FHMQX3GAABWSM0S2VZEC2SWC", @@ -550,8 +522,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account { Bot: util.Ptr(false), Locked: util.Ptr(true), Discoverable: util.Ptr(true), - Sensitive: util.Ptr(false), - Language: "en", URI: "http://example.org/users/Some_User", URL: "http://example.org/@Some_User", FetchedAt: time.Time{}, @@ -568,9 +538,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", - EnableRSS: util.Ptr(false), }, "remote_account_3": { ID: "062G5WYKY35KKD12EMSM3F8PJ8", @@ -586,8 +554,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account { Bot: util.Ptr(false), Locked: util.Ptr(true), Discoverable: util.Ptr(true), - Sensitive: util.Ptr(false), - Language: "en", URI: "http://thequeenisstillalive.technology/users/her_fuckin_maj", URL: "http://thequeenisstillalive.technology/@her_fuckin_maj", FetchedAt: time.Time{}, @@ -604,10 +570,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R", - EnableRSS: util.Ptr(false), }, "remote_account_4": { ID: "07GZRBAEMBNKGZ8Z9VSKSXKR98", @@ -622,8 +586,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account { Bot: util.Ptr(false), Locked: util.Ptr(false), Discoverable: util.Ptr(false), - Sensitive: util.Ptr(false), - Language: "de", URI: "https://xn--xample-ova.org/users/%C3%BCser", URL: "https://xn--xample-ova.org/users/@%C3%BCser", FetchedAt: time.Time{}, @@ -640,10 +602,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account { SensitizedAt: time.Time{}, SilencedAt: time.Time{}, SuspendedAt: time.Time{}, - HideCollections: util.Ptr(false), SuspensionOrigin: "", HeaderMediaAttachmentID: "", - EnableRSS: util.Ptr(false), }, } @@ -698,6 +658,55 @@ func NewTestAccounts() map[string]*gtsmodel.Account { return accounts } +func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings { + return map[string]*gtsmodel.AccountSettings{ + "unconfirmed_account": { + AccountID: "01F8MH0BBE4FHXPH513MBVFHB0", + CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"), + UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), + Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.", + Privacy: gtsmodel.VisibilityPublic, + Sensitive: util.Ptr(false), + Language: "en", + EnableRSS: util.Ptr(false), + HideCollections: util.Ptr(false), + }, + "admin_account": { + AccountID: "01F8MH17FWEB39HZJ76B6VXSKF", + CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"), + UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"), + Reason: "", + Privacy: gtsmodel.VisibilityPublic, + Sensitive: util.Ptr(false), + Language: "en", + EnableRSS: util.Ptr(true), + HideCollections: util.Ptr(false), + }, + "local_account_1": { + AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", + CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"), + UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"), + Reason: "I wanna be on this damned webbed site so bad! Please! Wow", + Privacy: gtsmodel.VisibilityPublic, + Sensitive: util.Ptr(false), + Language: "en", + EnableRSS: util.Ptr(true), + HideCollections: util.Ptr(false), + }, + "local_account_2": { + AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", + CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"), + UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"), + Reason: "", + Privacy: gtsmodel.VisibilityFollowersOnly, + Sensitive: util.Ptr(true), + Language: "fr", + EnableRSS: util.Ptr(false), + HideCollections: util.Ptr(false), + }, + } +} + func NewTestTombstones() map[string]*gtsmodel.Tombstone { return map[string]*gtsmodel.Tombstone{ "https://somewhere.mysterious/users/rest_in_piss#main-key": {