diff options
Diffstat (limited to 'internal')
33 files changed, 463 insertions, 146 deletions
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 - -	err := suite.db.UpdateAccount(context.Background(), a) +	// 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 + +	// 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 @@ import (  	"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 @@ var Defaults = Configuration{  		// 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 <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) +	} +} 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 <http://www.gnu.org/licenses/>. + +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 @@ import (  	"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 {  | 
