mirror of
1
Fork 0

[chore/refactor] start moving account preferences to its own separate struct

This commit is contained in:
tsmethurst 2023-08-10 16:33:00 +02:00
parent 9770d54237
commit 8a36e11957
18 changed files with 446 additions and 213 deletions

View File

@ -398,9 +398,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.EqualValues(requestingAccount.Reason, dbUpdatedAccount.Reason) suite.EqualValues(requestingAccount.Reason, dbUpdatedAccount.Reason)
suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked) suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked)
suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable) 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.URI, dbUpdatedAccount.URI)
suite.EqualValues(requestingAccount.URL, dbUpdatedAccount.URL) suite.EqualValues(requestingAccount.URL, dbUpdatedAccount.URL)
suite.EqualValues(requestingAccount.InboxURI, dbUpdatedAccount.InboxURI) suite.EqualValues(requestingAccount.InboxURI, dbUpdatedAccount.InboxURI)
@ -414,7 +411,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.EqualValues(requestingAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt) suite.EqualValues(requestingAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt)
suite.EqualValues(requestingAccount.SilencedAt, dbUpdatedAccount.SilencedAt) suite.EqualValues(requestingAccount.SilencedAt, dbUpdatedAccount.SilencedAt)
suite.EqualValues(requestingAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt) suite.EqualValues(requestingAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt)
suite.EqualValues(requestingAccount.HideCollections, dbUpdatedAccount.HideCollections)
suite.EqualValues(requestingAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin) suite.EqualValues(requestingAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin)
} }
@ -466,7 +462,6 @@ func (suite *InboxPostTestSuite) TestPostDelete() {
suite.Empty(dbAccount.HeaderRemoteURL) suite.Empty(dbAccount.HeaderRemoteURL)
suite.Empty(dbAccount.Reason) suite.Empty(dbAccount.Reason)
suite.Empty(dbAccount.Fields) suite.Empty(dbAccount.Fields)
suite.True(*dbAccount.HideCollections)
suite.False(*dbAccount.Discoverable) suite.False(*dbAccount.Discoverable)
suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second) suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second)
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)

View File

@ -477,7 +477,7 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceBadContentTypeFormDa
if err != nil { if err != nil {
suite.FailNow(err.Error()) suite.FailNow(err.Error())
} }
suite.Equal(data["source[status_content_type]"], dbAccount.StatusContentType) suite.Equal(data["source[status_content_type]"], dbAccount.Preferences.StatusContentType)
} }
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusContentTypeBad() { func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusContentTypeBad() {

View File

@ -81,7 +81,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
suite.Equal(2, apimodelAccount.FollowingCount) suite.Equal(2, apimodelAccount.FollowingCount)
suite.Equal(5, apimodelAccount.StatusesCount) suite.Equal(5, apimodelAccount.StatusesCount)
suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy) suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy)
suite.Equal(testAccount.Language, apimodelAccount.Source.Language) suite.Equal(testAccount.Preferences.StatusLanguage, apimodelAccount.Source.Language)
suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note) suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note)
} }

View File

@ -229,6 +229,7 @@ func (c *Caches) setuphooks() {
func (c *Caches) Sweep(threshold float64) { func (c *Caches) Sweep(threshold float64) {
c.GTS.Account().Trim(threshold) c.GTS.Account().Trim(threshold)
c.GTS.AccountNote().Trim(threshold) c.GTS.AccountNote().Trim(threshold)
c.GTS.AccountPreferences().Trim(threshold)
c.GTS.Block().Trim(threshold) c.GTS.Block().Trim(threshold)
c.GTS.BlockIDs().Trim(threshold) c.GTS.BlockIDs().Trim(threshold)
c.GTS.Emoji().Trim(threshold) c.GTS.Emoji().Trim(threshold)

27
internal/cache/gts.go vendored
View File

@ -32,6 +32,7 @@ import (
type GTSCaches struct { type GTSCaches struct {
account *result.Cache[*gtsmodel.Account] account *result.Cache[*gtsmodel.Account]
accountNote *result.Cache[*gtsmodel.AccountNote] accountNote *result.Cache[*gtsmodel.AccountNote]
accountPreferences *result.Cache[*gtsmodel.AccountPreferences]
block *result.Cache[*gtsmodel.Block] block *result.Cache[*gtsmodel.Block]
blockIDs *SliceCache[string] blockIDs *SliceCache[string]
boostOfIDs *SliceCache[string] boostOfIDs *SliceCache[string]
@ -67,6 +68,7 @@ type GTSCaches struct {
func (c *GTSCaches) Init() { func (c *GTSCaches) Init() {
c.initAccount() c.initAccount()
c.initAccountNote() c.initAccountNote()
c.initAccountPreferences()
c.initBlock() c.initBlock()
c.initBlockIDs() c.initBlockIDs()
c.initBoostOfIDs() c.initBoostOfIDs()
@ -117,6 +119,11 @@ func (c *GTSCaches) AccountNote() *result.Cache[*gtsmodel.AccountNote] {
return c.accountNote return c.accountNote
} }
// AccountPreferences provides access to the gtsmodel AccountPreferences database cache.
func (c *GTSCaches) AccountPreferences() *result.Cache[*gtsmodel.AccountPreferences] {
return c.accountPreferences
}
// Block provides access to the gtsmodel Block (account) database cache. // Block provides access to the gtsmodel Block (account) database cache.
func (c *GTSCaches) Block() *result.Cache[*gtsmodel.Block] { func (c *GTSCaches) Block() *result.Cache[*gtsmodel.Block] {
return c.block return c.block
@ -303,6 +310,26 @@ func (c *GTSCaches) initAccountNote() {
c.accountNote.IgnoreErrors(ignoreErrors) c.accountNote.IgnoreErrors(ignoreErrors)
} }
func (c *GTSCaches) initAccountPreferences() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
sizeofAccountPreferences(), // model in-mem size.
config.GetCacheAccountPreferencesMemRatio(),
)
log.Infof(nil, "AccountPreferences cache size = %d", cap)
c.accountPreferences = result.New([]result.Lookup{
{Name: "ID"},
{Name: "AccountID"},
}, func(p1 *gtsmodel.AccountPreferences) *gtsmodel.AccountPreferences {
p2 := new(gtsmodel.AccountPreferences)
*p2 = *p1
return p2
}, cap)
c.accountPreferences.IgnoreErrors(ignoreErrors)
}
func (c *GTSCaches) initBlock() { func (c *GTSCaches) initBlock() {
// Calculate maximum cache size. // Calculate maximum cache size.
cap := calculateResultCacheMax( cap := calculateResultCacheMax(

View File

@ -155,6 +155,7 @@ func totalOfRatios() float64 {
return 0 + return 0 +
config.GetCacheAccountMemRatio() + config.GetCacheAccountMemRatio() +
config.GetCacheAccountNoteMemRatio() + config.GetCacheAccountNoteMemRatio() +
config.GetCacheAccountPreferencesMemRatio() +
config.GetCacheBlockMemRatio() + config.GetCacheBlockMemRatio() +
config.GetCacheBlockIDsMemRatio() + config.GetCacheBlockIDsMemRatio() +
config.GetCacheBoostOfIDsMemRatio() + config.GetCacheBoostOfIDsMemRatio() +
@ -199,9 +200,6 @@ func sizeofAccount() uintptr {
Bot: func() *bool { ok := true; return &ok }(), Bot: func() *bool { ok := true; return &ok }(),
Locked: func() *bool { ok := true; return &ok }(), Locked: func() *bool { ok := true; return &ok }(),
Discoverable: func() *bool { ok := false; return &ok }(), Discoverable: func() *bool { ok := false; return &ok }(),
Privacy: gtsmodel.VisibilityFollowersOnly,
Sensitive: func() *bool { ok := true; return &ok }(),
Language: "fr",
URI: exampleURI, URI: exampleURI,
URL: exampleURI, URL: exampleURI,
InboxURI: exampleURI, InboxURI: exampleURI,
@ -216,9 +214,7 @@ func sizeofAccount() uintptr {
SensitizedAt: time.Time{}, SensitizedAt: time.Time{},
SilencedAt: time.Now(), SilencedAt: time.Now(),
SuspendedAt: time.Now(), SuspendedAt: time.Now(),
HideCollections: func() *bool { ok := true; return &ok }(),
SuspensionOrigin: "", SuspensionOrigin: "",
EnableRSS: func() *bool { ok := true; return &ok }(),
})) }))
} }
@ -231,6 +227,19 @@ func sizeofAccountNote() uintptr {
})) }))
} }
func sizeofAccountPreferences() uintptr {
return uintptr(size.Of(&gtsmodel.AccountPreferences{
ID: exampleID,
AccountID: exampleID,
StatusLanguage: "fr",
StatusPrivacy: gtsmodel.VisibilityFollowersOnly,
StatusSensitive: func() *bool { ok := true; return &ok }(),
StatusContentType: "text/plain",
HideCollections: func() *bool { ok := true; return &ok }(),
EnableRSS: func() *bool { ok := true; return &ok }(),
}))
}
func sizeofBlock() uintptr { func sizeofBlock() uintptr {
return uintptr(size.Of(&gtsmodel.Block{ return uintptr(size.Of(&gtsmodel.Block{
ID: exampleID, ID: exampleID,

View File

@ -178,6 +178,7 @@ type CacheConfiguration struct {
MemoryTarget bytesize.Size `name:"memory-target"` MemoryTarget bytesize.Size `name:"memory-target"`
AccountMemRatio float64 `name:"account-mem-ratio"` AccountMemRatio float64 `name:"account-mem-ratio"`
AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
AccountPreferencesMemRatio float64 `name:"account-preferences-mem-ratio"`
BlockMemRatio float64 `name:"block-mem-ratio"` BlockMemRatio float64 `name:"block-mem-ratio"`
BlockIDsMemRatio float64 `name:"block-mem-ratio"` BlockIDsMemRatio float64 `name:"block-mem-ratio"`
BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"` BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"`

View File

@ -147,6 +147,7 @@ var Defaults = Configuration{
// be able to make some more sense :D // be able to make some more sense :D
AccountMemRatio: 18, AccountMemRatio: 18,
AccountNoteMemRatio: 0.1, AccountNoteMemRatio: 0.1,
AccountPreferencesMemRatio: 2,
BlockMemRatio: 3, BlockMemRatio: 3,
BlockIDsMemRatio: 3, BlockIDsMemRatio: 3,
BoostOfIDsMemRatio: 3, BoostOfIDsMemRatio: 3,

View File

@ -2499,6 +2499,31 @@ func GetCacheAccountNoteMemRatio() float64 { return global.GetCacheAccountNoteMe
// SetCacheAccountNoteMemRatio safely sets the value for global configuration 'Cache.AccountNoteMemRatio' field // SetCacheAccountNoteMemRatio safely sets the value for global configuration 'Cache.AccountNoteMemRatio' field
func SetCacheAccountNoteMemRatio(v float64) { global.SetCacheAccountNoteMemRatio(v) } func SetCacheAccountNoteMemRatio(v float64) { global.SetCacheAccountNoteMemRatio(v) }
// GetCacheAccountPreferencesMemRatio safely fetches the Configuration value for state's 'Cache.AccountPreferencesMemRatio' field
func (st *ConfigState) GetCacheAccountPreferencesMemRatio() (v float64) {
st.mutex.RLock()
v = st.config.Cache.AccountPreferencesMemRatio
st.mutex.RUnlock()
return
}
// SetCacheAccountPreferencesMemRatio safely sets the Configuration value for state's 'Cache.AccountPreferencesMemRatio' field
func (st *ConfigState) SetCacheAccountPreferencesMemRatio(v float64) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.Cache.AccountPreferencesMemRatio = v
st.reloadToViper()
}
// CacheAccountPreferencesMemRatioFlag returns the flag name for the 'Cache.AccountPreferencesMemRatio' field
func CacheAccountPreferencesMemRatioFlag() string { return "cache-account-preferences-mem-ratio" }
// GetCacheAccountPreferencesMemRatio safely fetches the value for global configuration 'Cache.AccountPreferencesMemRatio' field
func GetCacheAccountPreferencesMemRatio() float64 { return global.GetCacheAccountPreferencesMemRatio() }
// SetCacheAccountPreferencesMemRatio safely sets the value for global configuration 'Cache.AccountPreferencesMemRatio' field
func SetCacheAccountPreferencesMemRatio(v float64) { global.SetCacheAccountPreferencesMemRatio(v) }
// GetCacheBlockMemRatio safely fetches the Configuration value for state's 'Cache.BlockMemRatio' field // GetCacheBlockMemRatio safely fetches the Configuration value for state's 'Cache.BlockMemRatio' field
func (st *ConfigState) GetCacheBlockMemRatio() (v float64) { func (st *ConfigState) GetCacheBlockMemRatio() (v float64) {
st.mutex.RLock() st.mutex.RLock()

View File

@ -70,6 +70,16 @@ type Account interface {
// GetAccountCustomCSSByUsername returns the custom css of an account on this instance with the given username. // GetAccountCustomCSSByUsername returns the custom css of an account on this instance with the given username.
GetAccountCustomCSSByUsername(ctx context.Context, username string) (string, error) GetAccountCustomCSSByUsername(ctx context.Context, username string) (string, error)
// GetAccountPreferencesByAccountID returns preferences the the given *LOCAL* account.
// Will return an error for non-local accounts, or accounts with no preferences stored.
GetAccountPreferencesByAccountID(ctx context.Context, accountID string) (*gtsmodel.AccountPreferences, error)
// PutAccountPreferences inserts the given accountPreferences.
PutAccountPreferences(ctx context.Context, accountPreferences *gtsmodel.AccountPreferences) error
// UpdateAccountPreferences updates the given accountPreferences by ID.
UpdateAccountPreferences(ctx context.Context, accountPreferences *gtsmodel.AccountPreferences, columns ...string) error
// GetAccountFaves fetches faves/likes created by the target accountID. // GetAccountFaves fetches faves/likes created by the target accountID.
GetAccountFaves(ctx context.Context, accountID string) ([]*gtsmodel.StatusFave, error) GetAccountFaves(ctx context.Context, accountID string) ([]*gtsmodel.StatusFave, error)

View File

@ -290,6 +290,16 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
} }
} }
if account.PreferencesID != "" && account.Preferences == nil {
account.Preferences, err = a.getAccountPreferencesByID(
ctx, // these are already barebones
account.PreferencesID,
)
if err != nil {
errs.Appendf("error populating account preferences: %w", err)
}
}
return errs.Combine() return errs.Combine()
} }
@ -456,12 +466,93 @@ func (a *accountDB) SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachmen
} }
func (a *accountDB) GetAccountCustomCSSByUsername(ctx context.Context, username string) (string, error) { func (a *accountDB) GetAccountCustomCSSByUsername(ctx context.Context, username string) (string, error) {
account, err := a.GetAccountByUsernameDomain(ctx, username, "") account, err := a.GetAccountByUsernameDomain(
gtscontext.SetBarebones(ctx),
username,
"",
)
if err != nil { if err != nil {
return "", err return "", err
} }
return account.CustomCSS, nil if account.PreferencesID == "" {
return "", gtserror.Newf("no preferences stored for account %s", account.ID)
}
accountPrefs, err := a.state.DB.GetAccountPreferencesByAccountID(ctx, account.ID)
if err != nil {
return "", gtserror.Newf("error getting preferences for account %s: %w", account.ID, err)
}
return accountPrefs.CustomCSS, nil
}
func (a *accountDB) GetAccountPreferencesByAccountID(ctx context.Context, accountID string) (*gtsmodel.AccountPreferences, error) {
accountPrefs, err := a.state.Caches.GTS.AccountPreferences().Load("AccountID", func() (*gtsmodel.AccountPreferences, error) {
var accountPrefs gtsmodel.AccountPreferences
// Not cached! Perform database query
if err := a.db.NewSelect().
Model(&accountPrefs).
Where("? = ?", bun.Ident("account_preferences.account_id"), accountID).
Scan(ctx); err != nil {
return nil, a.db.ProcessError(err)
}
return &accountPrefs, nil
}, accountID)
if err != nil {
return nil, err
}
return accountPrefs, nil
}
func (a *accountDB) getAccountPreferencesByID(ctx context.Context, id string) (*gtsmodel.AccountPreferences, error) {
accountPrefs, err := a.state.Caches.GTS.AccountPreferences().Load("ID", func() (*gtsmodel.AccountPreferences, error) {
var accountPrefs gtsmodel.AccountPreferences
// Not cached! Perform database query
if err := a.db.NewSelect().
Model(&accountPrefs).
Where("? = ?", bun.Ident("account_preferences.id"), id).
Scan(ctx); err != nil {
return nil, a.db.ProcessError(err)
}
return &accountPrefs, nil
}, id)
if err != nil {
return nil, err
}
return accountPrefs, nil
}
func (a *accountDB) PutAccountPreferences(ctx context.Context, accountPrefs *gtsmodel.AccountPreferences) error {
return a.state.Caches.GTS.AccountPreferences().Store(accountPrefs, func() error {
// insert the account preferences
_, err := a.db.NewInsert().Model(accountPrefs).Exec(ctx)
return a.db.ProcessError(err)
})
}
func (a *accountDB) UpdateAccountPreferences(ctx context.Context, accountPrefs *gtsmodel.AccountPreferences, columns ...string) error {
accountPrefs.UpdatedAt = time.Now()
if len(columns) > 0 {
// If we're updating by column, ensure "updated_at" is included.
columns = append(columns, "updated_at")
}
return a.state.Caches.GTS.AccountPreferences().Store(accountPrefs, func() error {
_, err := a.db.
NewUpdate().
Model(accountPrefs).
Where("? = ?", bun.Ident("account_preferences.id"), accountPrefs.ID).
Column(columns...).
Exec(ctx)
return a.db.ProcessError(err)
})
} }
func (a *accountDB) GetAccountsUsingEmoji(ctx context.Context, emojiID string) ([]*gtsmodel.Account, error) { func (a *accountDB) GetAccountsUsingEmoji(ctx context.Context, emojiID string) ([]*gtsmodel.Account, error) {

View File

@ -333,14 +333,11 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
err = suite.db.Put(context.Background(), newAccount) err = suite.db.Put(context.Background(), newAccount)
suite.NoError(err) suite.NoError(err)
suite.Equal("en", newAccount.Language)
suite.WithinDuration(time.Now(), newAccount.CreatedAt, 30*time.Second) suite.WithinDuration(time.Now(), newAccount.CreatedAt, 30*time.Second)
suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second) suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second)
suite.False(*newAccount.Memorial) suite.False(*newAccount.Memorial)
suite.False(*newAccount.Bot) suite.False(*newAccount.Bot)
suite.False(*newAccount.Discoverable) suite.False(*newAccount.Discoverable)
suite.False(*newAccount.Sensitive)
suite.False(*newAccount.HideCollections)
} }
func (suite *AccountTestSuite) TestGetAccountPinnedStatusesSomeResults() { func (suite *AccountTestSuite) TestGetAccountPinnedStatusesSomeResults() {

View File

@ -30,6 +30,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
@ -95,7 +96,10 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
// If something went wrong previously while doing a new // If something went wrong previously while doing a new
// sign up with this username, we might already have an // sign up with this username, we might already have an
// account, so check first. // account, so check first.
account, err := a.state.DB.GetAccountByUsernameDomain(ctx, newSignup.Username, "") account, err := a.state.DB.GetAccountByUsernameDomain(
gtscontext.SetBarebones(ctx),
newSignup.Username, "",
)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {
// Real error occurred. // Real error occurred.
err := gtserror.Newf("error checking for existing account: %w", err) err := gtserror.Newf("error checking for existing account: %w", err)
@ -124,7 +128,6 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
Username: newSignup.Username, Username: newSignup.Username,
DisplayName: newSignup.Username, DisplayName: newSignup.Username,
Reason: newSignup.Reason, Reason: newSignup.Reason,
Privacy: gtsmodel.VisibilityDefault,
URI: uris.UserURI, URI: uris.UserURI,
URL: uris.UserURL, URL: uris.UserURL,
InboxURI: uris.InboxURI, InboxURI: uris.InboxURI,
@ -136,6 +139,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
PrivateKey: privKey, PrivateKey: privKey,
PublicKey: &privKey.PublicKey, PublicKey: &privKey.PublicKey,
PublicKeyURI: uris.PublicKeyURI, PublicKeyURI: uris.PublicKeyURI,
PreferencesID: id.NewULID(),
} }
// Insert the new account! // Insert the new account!
@ -145,6 +149,33 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
} }
// Created or already had an account. // Created or already had an account.
// Create account preferences
// if not done already.
accountPrefs, err := a.state.DB.GetAccountPreferencesByAccountID(ctx, account.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// Real error.
err := gtserror.Newf("error checking for existing account preferences: %w", err)
return nil, err
}
if accountPrefs == nil {
// No preferences created for
// this account yet, do it now.
accountPrefs = &gtsmodel.AccountPreferences{
ID: account.PreferencesID,
AccountID: account.ID,
StatusPrivacy: gtsmodel.VisibilityDefault,
StatusLanguage: newSignup.Locale,
}
if err := a.state.DB.PutAccountPreferences(ctx, accountPrefs); err != nil {
err := gtserror.Newf("db error inserting account preferences: %w", err)
return nil, err
}
account.Preferences = accountPrefs
}
// Ensure user not already created. // Ensure user not already created.
user, err := a.state.DB.GetUserByAccountID(ctx, account.ID) user, err := a.state.DB.GetUserByAccountID(ctx, account.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {

View File

@ -94,10 +94,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
// to true, which is why we use pointers for bools in the first place // to true, which is why we use pointers for bools in the first place
suite.True(*a.Locked) suite.True(*a.Locked)
suite.False(*a.Discoverable) 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.URI, a.URI)
suite.Equal(testAccount.URL, a.URL) suite.Equal(testAccount.URL, a.URL)
suite.Zero(testAccount.FetchedAt) suite.Zero(testAccount.FetchedAt)
@ -113,7 +109,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
suite.Zero(a.SensitizedAt) suite.Zero(a.SensitizedAt)
suite.Zero(a.SilencedAt) suite.Zero(a.SilencedAt)
suite.Zero(a.SuspendedAt) suite.Zero(a.SuspendedAt)
suite.False(*a.HideCollections)
suite.Empty(a.SuspensionOrigin) suite.Empty(a.SuspensionOrigin)
} }

View File

@ -47,8 +47,8 @@ type Account struct {
DisplayName string `bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. 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 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 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. Fields []*Field `bun:""` // 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 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) 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 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? Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away?
@ -58,11 +58,6 @@ type Account struct {
Reason string `bun:""` // What reason was given for signing up when this account was created? 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? 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? 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. URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI for this account.
URL string `bun:",nullzero,unique"` // Web URL for this account's profile 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 InboxURI string `bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to
@ -78,9 +73,9 @@ type Account struct {
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive? 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)? 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) 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 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 PreferencesID string `bun:"type:CHAR(26),nullzero"` // ID of the LocalAccountPreferences entry for this account. Only set for local accounts.
Preferences *AccountPreferences `bun:"-"` // Preferences corresponding to PreferencesID. Local accounts only.
} }
// IsLocal returns whether account is a local user account. // IsLocal returns whether account is a local user account.

View File

@ -0,0 +1,34 @@
// 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"
type AccountPreferences struct {
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Creation time of this item.
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // Last updated time of this time.
AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of the account to which these preferences correspond.
StatusLanguage string `bun:",nullzero,notnull,default:'en'"` // Default post language for this account.
StatusPrivacy Visibility `bun:",nullzero"` // Default post privacy for this account.
StatusSensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default.
StatusContentType string `bun:",nullzero"` // Default format for statuses posted by this account.
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's collections.
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // Enable RSS feed subscription for this account's public posts at [URL]/feed
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
}

View File

@ -114,14 +114,6 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a
memorial := false memorial := false
acct.Memorial = &memorial acct.Memorial = &memorial
// assume not sensitive (todo)
sensitive := false
acct.Sensitive = &sensitive
// assume not hide collections (todo)
hideCollections := false
acct.HideCollections = &hideCollections
// locked aka manuallyApprovesFollowers // locked aka manuallyApprovesFollowers
locked := true locked := true
acct.Locked = &locked // assume locked by default acct.Locked = &locked // assume locked by default
@ -140,10 +132,6 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a
acct.Discoverable = &d acct.Discoverable = &d
} }
// assume not rss feed
enableRSS := false
acct.EnableRSS = &enableRSS
// url property // url property
url, err := ap.ExtractURL(accountable) url, err := ap.ExtractURL(accountable)
if err == nil { if err == nil {

View File

@ -59,29 +59,47 @@ func toMastodonVersion(in string) string {
} }
func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
// we can build this sensitive account easily by first getting the public account.... // Build sensitive view of account by first
// getting the public account and then adding
// additional information for the eyes of this
// account owner only.
apiAccount, err := c.AccountToAPIAccountPublic(ctx, a) apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// then adding the Source object to it... // Ensure account preferences populated.
// Accounts passed in to this function
// check pending follow requests aimed at this account // should always be local accounts, so
frc, err := c.db.CountAccountFollowRequests(ctx, a.ID) // a missing account preferences struct
// is a real problem!
if a.Preferences == nil {
var err error
a.Preferences, err = c.db.GetAccountPreferencesByAccountID(ctx, a.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error counting follow requests: %s", err) return nil, gtserror.Newf("error getting account preferences: %w", err)
}
} }
statusContentType := string(apimodel.StatusContentTypeDefault) // Count pending follow requests aimed at this account.
if a.StatusContentType != "" { frc, err := c.db.CountAccountFollowRequests(ctx, a.ID)
statusContentType = a.StatusContentType if err != nil {
return nil, gtserror.Newf("error counting follow requests: %w", err)
}
// Use default status content type
// if account has no other preference.
var statusContentType string
if a.Preferences.StatusContentType != "" {
statusContentType = a.Preferences.StatusContentType
} else {
statusContentType = string(apimodel.StatusContentTypeDefault)
} }
apiAccount.Source = &apimodel.Source{ apiAccount.Source = &apimodel.Source{
Privacy: c.VisToAPIVis(ctx, a.Privacy), Privacy: c.VisToAPIVis(ctx, a.Preferences.StatusPrivacy),
Sensitive: *a.Sensitive, Sensitive: *a.Preferences.StatusSensitive,
Language: a.Language, Language: a.Preferences.StatusLanguage,
StatusContentType: statusContentType, StatusContentType: statusContentType,
Note: a.NoteRaw, Note: a.NoteRaw,
Fields: c.fieldsToAPIFields(a.FieldsRaw), Fields: c.fieldsToAPIFields(a.FieldsRaw),
@ -104,23 +122,23 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
followersCount, err := c.db.CountAccountFollowers(ctx, a.ID) followersCount, err := c.db.CountAccountFollowers(ctx, a.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting followers: %w", err) return nil, gtserror.Newf("error counting followers: %w", err)
} }
followingCount, err := c.db.CountAccountFollows(ctx, a.ID) followingCount, err := c.db.CountAccountFollows(ctx, a.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting following: %w", err) return nil, gtserror.Newf("error counting following: %w", err)
} }
statusesCount, err := c.db.CountAccountStatuses(ctx, a.ID) statusesCount, err := c.db.CountAccountStatuses(ctx, a.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err) return nil, gtserror.Newf("error counting statuses: %w", err)
} }
var lastStatusAt *string var lastStatusAt *string
lastPosted, err := c.db.GetAccountLastPosted(ctx, a.ID, false) lastPosted, err := c.db.GetAccountLastPosted(ctx, a.ID, false)
if err != nil && !errors.Is(err, db.ErrNoEntries) { if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error counting statuses: %w", err) return nil, gtserror.Newf("error counting statuses: %w", err)
} }
if !lastPosted.IsZero() { if !lastPosted.IsZero() {
@ -166,25 +184,32 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
var ( var (
acct string acct string
role *apimodel.AccountRole role *apimodel.AccountRole
customCSS string
enableRSS bool
) )
if a.IsRemote() { switch {
// Domain may be in Punycode,
// de-punify it just in case. // Only extra thing we can do for
// remote accounts is properly
// de-punify their domain.
case a.IsRemote():
d, err := util.DePunify(a.Domain) d, err := util.DePunify(a.Domain)
if err != nil { if err != nil {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error de-punifying domain %s for account id %s: %w", a.Domain, a.ID, err) return nil, gtserror.Newf("error de-punifying domain %s for account %s: %w", a.Domain, a.ID, err)
} }
acct = a.Username + "@" + d acct = a.Username + "@" + d
} else {
// This is a local account, try to // This is a non-instance local
// fetch more info. Skip for instance // account; fetch more info using
// accounts since they have no user. // the account User. We skip this
if !a.IsInstance() { // step for instance accounts since
// they have no user.
case !a.IsInstance():
user, err := c.db.GetUserByAccountID(ctx, a.ID) user, err := c.db.GetUserByAccountID(ctx, a.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %w", a.ID, err) return nil, gtserror.Newf("db error getting user for account %s: %w", a.ID, err)
} }
switch { switch {
@ -195,9 +220,17 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
default: default:
role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser} role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser}
} }
}
acct = a.Username // omit domain fallthrough // to below lines
// Fetch info which applies to
// local instance account and
// to actual local accounts.
default:
if a.Preferences != nil {
customCSS = a.Preferences.CustomCSS
enableRSS = *a.Preferences.EnableRSS
}
} }
// Remaining properties are simple and // Remaining properties are simple and
@ -225,8 +258,8 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
Emojis: apiEmojis, Emojis: apiEmojis,
Fields: fields, Fields: fields,
Suspended: !a.SuspendedAt.IsZero(), Suspended: !a.SuspendedAt.IsZero(),
CustomCSS: a.CustomCSS, CustomCSS: customCSS,
EnableRSS: *a.EnableRSS, EnableRSS: enableRSS,
Role: role, Role: role,
} }
@ -279,7 +312,7 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
if !a.IsInstance() { if !a.IsInstance() {
user, err := c.db.GetUserByAccountID(ctx, a.ID) user, err := c.db.GetUserByAccountID(ctx, a.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %w", a.ID, err) return nil, gtserror.Newf("error getting user from database for account id %s: %w", a.ID, err)
} }
switch { switch {