From 8a36e11957b1767cbbe8963503d3ace12f065bd9 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Thu, 10 Aug 2023 16:33:00 +0200 Subject: [PATCH] [chore/refactor] start moving account preferences to its own separate struct --- .../api/activitypub/users/inboxpost_test.go | 5 - .../api/client/accounts/accountupdate_test.go | 2 +- .../api/client/accounts/accountverify_test.go | 2 +- internal/cache/cache.go | 1 + internal/cache/gts.go | 81 ++++++++---- internal/cache/size.go | 19 ++- internal/config/config.go | 59 ++++----- internal/config/defaults.go | 57 +++++---- internal/config/helpers.gen.go | 25 ++++ internal/db/account.go | 10 ++ internal/db/bundb/account.go | 95 +++++++++++++- internal/db/bundb/account_test.go | 3 - internal/db/bundb/admin.go | 35 ++++- internal/db/bundb/basic_test.go | 5 - internal/gtsmodel/account.go | 93 +++++++------- internal/gtsmodel/accountpreferences.go | 34 +++++ internal/typeutils/astointernal.go | 12 -- internal/typeutils/internaltofrontend.go | 121 +++++++++++------- 18 files changed, 446 insertions(+), 213 deletions(-) create mode 100644 internal/gtsmodel/accountpreferences.go diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go index d26dae513..0297a3cd2 100644 --- a/internal/api/activitypub/users/inboxpost_test.go +++ b/internal/api/activitypub/users/inboxpost_test.go @@ -398,9 +398,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { 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 +411,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) } @@ -466,7 +462,6 @@ func (suite *InboxPostTestSuite) TestPostDelete() { 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 835989037..b9963d792 100644 --- a/internal/api/client/accounts/accountupdate_test.go +++ b/internal/api/client/accounts/accountupdate_test.go @@ -477,7 +477,7 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceBadContentTypeFormDa if err != nil { 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() { diff --git a/internal/api/client/accounts/accountverify_test.go b/internal/api/client/accounts/accountverify_test.go index 1690a0271..935e2deff 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(5, apimodelAccount.StatusesCount) 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) } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index ec0ec3faa..c2fd3be3c 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -229,6 +229,7 @@ func (c *Caches) setuphooks() { func (c *Caches) Sweep(threshold float64) { c.GTS.Account().Trim(threshold) c.GTS.AccountNote().Trim(threshold) + c.GTS.AccountPreferences().Trim(threshold) c.GTS.Block().Trim(threshold) c.GTS.BlockIDs().Trim(threshold) c.GTS.Emoji().Trim(threshold) diff --git a/internal/cache/gts.go b/internal/cache/gts.go index f120bcf4e..51b89db8f 100644 --- a/internal/cache/gts.go +++ b/internal/cache/gts.go @@ -30,33 +30,34 @@ import ( ) type GTSCaches struct { - account *result.Cache[*gtsmodel.Account] - accountNote *result.Cache[*gtsmodel.AccountNote] - block *result.Cache[*gtsmodel.Block] - blockIDs *SliceCache[string] - boostOfIDs *SliceCache[string] - domainBlock *domain.BlockCache - emoji *result.Cache[*gtsmodel.Emoji] - emojiCategory *result.Cache[*gtsmodel.EmojiCategory] - follow *result.Cache[*gtsmodel.Follow] - followIDs *SliceCache[string] - followRequest *result.Cache[*gtsmodel.FollowRequest] - followRequestIDs *SliceCache[string] - instance *result.Cache[*gtsmodel.Instance] - inReplyToIDs *SliceCache[string] - list *result.Cache[*gtsmodel.List] - listEntry *result.Cache[*gtsmodel.ListEntry] - marker *result.Cache[*gtsmodel.Marker] - media *result.Cache[*gtsmodel.MediaAttachment] - mention *result.Cache[*gtsmodel.Mention] - notification *result.Cache[*gtsmodel.Notification] - report *result.Cache[*gtsmodel.Report] - status *result.Cache[*gtsmodel.Status] - statusFave *result.Cache[*gtsmodel.StatusFave] - statusFaveIDs *SliceCache[string] - tag *result.Cache[*gtsmodel.Tag] - tombstone *result.Cache[*gtsmodel.Tombstone] - user *result.Cache[*gtsmodel.User] + account *result.Cache[*gtsmodel.Account] + accountNote *result.Cache[*gtsmodel.AccountNote] + accountPreferences *result.Cache[*gtsmodel.AccountPreferences] + block *result.Cache[*gtsmodel.Block] + blockIDs *SliceCache[string] + boostOfIDs *SliceCache[string] + domainBlock *domain.BlockCache + emoji *result.Cache[*gtsmodel.Emoji] + emojiCategory *result.Cache[*gtsmodel.EmojiCategory] + follow *result.Cache[*gtsmodel.Follow] + followIDs *SliceCache[string] + followRequest *result.Cache[*gtsmodel.FollowRequest] + followRequestIDs *SliceCache[string] + instance *result.Cache[*gtsmodel.Instance] + inReplyToIDs *SliceCache[string] + list *result.Cache[*gtsmodel.List] + listEntry *result.Cache[*gtsmodel.ListEntry] + marker *result.Cache[*gtsmodel.Marker] + media *result.Cache[*gtsmodel.MediaAttachment] + mention *result.Cache[*gtsmodel.Mention] + notification *result.Cache[*gtsmodel.Notification] + report *result.Cache[*gtsmodel.Report] + status *result.Cache[*gtsmodel.Status] + statusFave *result.Cache[*gtsmodel.StatusFave] + statusFaveIDs *SliceCache[string] + tag *result.Cache[*gtsmodel.Tag] + tombstone *result.Cache[*gtsmodel.Tombstone] + user *result.Cache[*gtsmodel.User] // TODO: move out of GTS caches since unrelated to DB. webfinger *ttl.Cache[string, string] // TTL=24hr, sweep=5min @@ -67,6 +68,7 @@ type GTSCaches struct { func (c *GTSCaches) Init() { c.initAccount() c.initAccountNote() + c.initAccountPreferences() c.initBlock() c.initBlockIDs() c.initBoostOfIDs() @@ -117,6 +119,11 @@ func (c *GTSCaches) AccountNote() *result.Cache[*gtsmodel.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. func (c *GTSCaches) Block() *result.Cache[*gtsmodel.Block] { return c.block @@ -303,6 +310,26 @@ func (c *GTSCaches) initAccountNote() { 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() { // Calculate maximum cache size. cap := calculateResultCacheMax( diff --git a/internal/cache/size.go b/internal/cache/size.go index ec7c554c0..f86826c41 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -155,6 +155,7 @@ func totalOfRatios() float64 { return 0 + config.GetCacheAccountMemRatio() + config.GetCacheAccountNoteMemRatio() + + config.GetCacheAccountPreferencesMemRatio() + config.GetCacheBlockMemRatio() + config.GetCacheBlockIDsMemRatio() + config.GetCacheBoostOfIDsMemRatio() + @@ -199,9 +200,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, @@ -216,9 +214,7 @@ func sizeofAccount() uintptr { SensitizedAt: time.Time{}, SilencedAt: time.Now(), SuspendedAt: time.Now(), - HideCollections: func() *bool { ok := true; return &ok }(), SuspensionOrigin: "", - EnableRSS: func() *bool { ok := true; return &ok }(), })) } @@ -231,6 +227,19 @@ func sizeofAccountNote() uintptr { })) } +func sizeofAccountPreferences() uintptr { + return uintptr(size.Of(>smodel.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 { return uintptr(size.Of(>smodel.Block{ ID: exampleID, diff --git a/internal/config/config.go b/internal/config/config.go index ef79d4e12..358101110 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -175,35 +175,36 @@ type HTTPClientConfiguration struct { } type CacheConfiguration struct { - MemoryTarget bytesize.Size `name:"memory-target"` - AccountMemRatio float64 `name:"account-mem-ratio"` - AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` - BlockMemRatio float64 `name:"block-mem-ratio"` - BlockIDsMemRatio float64 `name:"block-mem-ratio"` - BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"` - EmojiMemRatio float64 `name:"emoji-mem-ratio"` - EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"` - FollowMemRatio float64 `name:"follow-mem-ratio"` - FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"` - FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"` - FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"` - InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"` - InstanceMemRatio float64 `name:"instance-mem-ratio"` - ListMemRatio float64 `name:"list-mem-ratio"` - ListEntryMemRatio float64 `name:"list-entry-mem-ratio"` - MarkerMemRatio float64 `name:"marker-mem-ratio"` - MediaMemRatio float64 `name:"media-mem-ratio"` - MentionMemRatio float64 `name:"mention-mem-ratio"` - NotificationMemRatio float64 `name:"notification-mem-ratio"` - ReportMemRatio float64 `name:"report-mem-ratio"` - StatusMemRatio float64 `name:"status-mem-ratio"` - StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"` - StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"` - TagMemRatio float64 `name:"tag-mem-ratio"` - TombstoneMemRatio float64 `name:"tombstone-mem-ratio"` - UserMemRatio float64 `name:"user-mem-ratio"` - WebfingerMemRatio float64 `name:"webfinger-mem-ratio"` - VisibilityMemRatio float64 `name:"visibility-mem-ratio"` + MemoryTarget bytesize.Size `name:"memory-target"` + AccountMemRatio float64 `name:"account-mem-ratio"` + AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` + AccountPreferencesMemRatio float64 `name:"account-preferences-mem-ratio"` + BlockMemRatio float64 `name:"block-mem-ratio"` + BlockIDsMemRatio float64 `name:"block-mem-ratio"` + BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"` + EmojiMemRatio float64 `name:"emoji-mem-ratio"` + EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"` + FollowMemRatio float64 `name:"follow-mem-ratio"` + FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"` + FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"` + FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"` + InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"` + InstanceMemRatio float64 `name:"instance-mem-ratio"` + ListMemRatio float64 `name:"list-mem-ratio"` + ListEntryMemRatio float64 `name:"list-entry-mem-ratio"` + MarkerMemRatio float64 `name:"marker-mem-ratio"` + MediaMemRatio float64 `name:"media-mem-ratio"` + MentionMemRatio float64 `name:"mention-mem-ratio"` + NotificationMemRatio float64 `name:"notification-mem-ratio"` + ReportMemRatio float64 `name:"report-mem-ratio"` + StatusMemRatio float64 `name:"status-mem-ratio"` + StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"` + StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"` + TagMemRatio float64 `name:"tag-mem-ratio"` + TombstoneMemRatio float64 `name:"tombstone-mem-ratio"` + UserMemRatio float64 `name:"user-mem-ratio"` + WebfingerMemRatio float64 `name:"webfinger-mem-ratio"` + VisibilityMemRatio float64 `name:"visibility-mem-ratio"` } // MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML). diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 2bc95f6f1..4c1cf15b3 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -145,34 +145,35 @@ var Defaults = Configuration{ // when TODO items in the size.go source // file have been addressed, these should // be able to make some more sense :D - AccountMemRatio: 18, - AccountNoteMemRatio: 0.1, - BlockMemRatio: 3, - BlockIDsMemRatio: 3, - BoostOfIDsMemRatio: 3, - EmojiMemRatio: 3, - EmojiCategoryMemRatio: 0.1, - FollowMemRatio: 4, - FollowIDsMemRatio: 4, - FollowRequestMemRatio: 2, - FollowRequestIDsMemRatio: 2, - InReplyToIDsMemRatio: 3, - InstanceMemRatio: 1, - ListMemRatio: 3, - ListEntryMemRatio: 3, - MarkerMemRatio: 0.5, - MediaMemRatio: 4, - MentionMemRatio: 5, - NotificationMemRatio: 5, - ReportMemRatio: 1, - StatusMemRatio: 18, - StatusFaveMemRatio: 5, - StatusFaveIDsMemRatio: 3, - TagMemRatio: 3, - TombstoneMemRatio: 2, - UserMemRatio: 0.1, - WebfingerMemRatio: 0.1, - VisibilityMemRatio: 2, + AccountMemRatio: 18, + AccountNoteMemRatio: 0.1, + AccountPreferencesMemRatio: 2, + BlockMemRatio: 3, + BlockIDsMemRatio: 3, + BoostOfIDsMemRatio: 3, + EmojiMemRatio: 3, + EmojiCategoryMemRatio: 0.1, + FollowMemRatio: 4, + FollowIDsMemRatio: 4, + FollowRequestMemRatio: 2, + FollowRequestIDsMemRatio: 2, + InReplyToIDsMemRatio: 3, + InstanceMemRatio: 1, + ListMemRatio: 3, + ListEntryMemRatio: 3, + MarkerMemRatio: 0.5, + MediaMemRatio: 4, + MentionMemRatio: 5, + NotificationMemRatio: 5, + ReportMemRatio: 1, + StatusMemRatio: 18, + StatusFaveMemRatio: 5, + StatusFaveIDsMemRatio: 3, + TagMemRatio: 3, + TombstoneMemRatio: 2, + UserMemRatio: 0.1, + WebfingerMemRatio: 0.1, + VisibilityMemRatio: 2, }, HTTPClient: HTTPClientConfiguration{ diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index 0a299e7d0..f0e9ee748 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -2499,6 +2499,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) } +// 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 func (st *ConfigState) GetCacheBlockMemRatio() (v float64) { st.mutex.RLock() diff --git a/internal/db/account.go b/internal/db/account.go index 505ca4004..8c8ae5b3f 100644 --- a/internal/db/account.go +++ b/internal/db/account.go @@ -70,6 +70,16 @@ type Account interface { // GetAccountCustomCSSByUsername returns the custom css of an account on this instance with the given username. 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(ctx context.Context, accountID string) ([]*gtsmodel.StatusFave, error) diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 2d9a64454..d2f728435 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -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() } @@ -456,12 +466,93 @@ func (a *accountDB) SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachmen } 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 { 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) { diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index b410bb3ed..2d93003d1 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -333,14 +333,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.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 723648a9e..8ad8fc26a 100644 --- a/internal/db/bundb/admin.go +++ b/internal/db/bundb/admin.go @@ -30,6 +30,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "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 // sign up with this username, we might already have an // 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) { // Real error occurred. 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, DisplayName: newSignup.Username, Reason: newSignup.Reason, - Privacy: gtsmodel.VisibilityDefault, URI: uris.UserURI, URL: uris.UserURL, InboxURI: uris.InboxURI, @@ -136,6 +139,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) ( PrivateKey: privKey, PublicKey: &privKey.PublicKey, PublicKeyURI: uris.PublicKeyURI, + PreferencesID: id.NewULID(), } // Insert the new account! @@ -145,6 +149,33 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) ( } // 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 = >smodel.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. user, err := a.state.DB.GetUserByAccountID(ctx, account.ID) if err != nil && !errors.Is(err, db.ErrNoEntries) { diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go index a24deac9e..d2c83dff8 100644 --- a/internal/db/bundb/basic_test.go +++ b/internal/db/bundb/basic_test.go @@ -94,10 +94,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() { // 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 +109,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/gtsmodel/account.go b/internal/gtsmodel/account.go index 7b27f076a..a8ffe3665 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -32,55 +32,50 @@ import ( // Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc). type Account 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"` // when was item created. - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated. - FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched. - Username string `bun:",nullzero,notnull,unique:usernamedomain"` // Username of the account, should just be a string of [a-zA-Z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``. Username and domain should be unique *with* each other - Domain string `bun:",nullzero,unique:usernamedomain"` // Domain of the account, will be null if this is a local account, otherwise something like ``example.org``. Should be unique with username. - AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present - AvatarMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to avatarMediaAttachmentID - AvatarRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched? - HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present - HeaderMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID - HeaderRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched? - 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? - AlsoKnownAs string `bun:"type:CHAR(26),nullzero"` // This account is associated with x account id (TODO: migrate to be AlsoKnownAsID) - MovedToAccountID string `bun:"type:CHAR(26),nullzero"` // This account has moved this account id in the database - 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 validating activitypub requests, will only be defined for local accounts - PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for encoding 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 - 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 + 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"` // when was item created. + UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated. + FetchedAt time.Time `bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched. + Username string `bun:",nullzero,notnull,unique:usernamedomain"` // Username of the account, should just be a string of [a-zA-Z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``. Username and domain should be unique *with* each other + Domain string `bun:",nullzero,unique:usernamedomain"` // Domain of the account, will be null if this is a local account, otherwise something like ``example.org``. Should be unique with username. + AvatarMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present + AvatarMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to avatarMediaAttachmentID + AvatarRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched? + HeaderMediaAttachmentID string `bun:"type:CHAR(26),nullzero"` // Database ID of the media attachment, if present + HeaderMediaAttachment *MediaAttachment `bun:"rel:belongs-to"` // MediaAttachment corresponding to headerMediaAttachmentID + HeaderRemoteURL string `bun:",nullzero"` // For a non-local account, where can the header be fetched? + 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 `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? + AlsoKnownAs string `bun:"type:CHAR(26),nullzero"` // This account is associated with x account id (TODO: migrate to be AlsoKnownAsID) + MovedToAccountID string `bun:"type:CHAR(26),nullzero"` // This account has moved this account id in the database + 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? + 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 validating activitypub requests, will only be defined for local accounts + PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for encoding 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 + 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 + 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. diff --git a/internal/gtsmodel/accountpreferences.go b/internal/gtsmodel/accountpreferences.go new file mode 100644 index 000000000..0b28d5295 --- /dev/null +++ b/internal/gtsmodel/accountpreferences.go @@ -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 . + +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. +} diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 7b1ba0396..fb10a0779 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -114,14 +114,6 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a memorial := false 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 := true acct.Locked = &locked // assume locked by default @@ -140,10 +132,6 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a acct.Discoverable = &d } - // assume not rss feed - enableRSS := false - acct.EnableRSS = &enableRSS - // url property url, err := ap.ExtractURL(accountable) if err == nil { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 2dc0e4dd5..6f7f6c153 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -59,29 +59,47 @@ func toMastodonVersion(in string) string { } 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) if err != nil { return nil, err } - // then adding the Source object to it... - - // check pending follow requests aimed at this account - frc, err := c.db.CountAccountFollowRequests(ctx, a.ID) - if err != nil { - return nil, fmt.Errorf("error counting follow requests: %s", err) + // Ensure account preferences populated. + // Accounts passed in to this function + // should always be local accounts, so + // 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 { + return nil, gtserror.Newf("error getting account preferences: %w", err) + } } - statusContentType := string(apimodel.StatusContentTypeDefault) - if a.StatusContentType != "" { - statusContentType = a.StatusContentType + // Count pending follow requests aimed at this account. + frc, err := c.db.CountAccountFollowRequests(ctx, a.ID) + 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{ - Privacy: c.VisToAPIVis(ctx, a.Privacy), - Sensitive: *a.Sensitive, - Language: a.Language, + Privacy: c.VisToAPIVis(ctx, a.Preferences.StatusPrivacy), + Sensitive: *a.Preferences.StatusSensitive, + Language: a.Preferences.StatusLanguage, StatusContentType: statusContentType, Note: a.NoteRaw, 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) 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) 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) 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 lastPosted, err := c.db.GetAccountLastPosted(ctx, a.ID, false) 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() { @@ -164,40 +182,55 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A // - Role. var ( - acct string - role *apimodel.AccountRole + acct string + role *apimodel.AccountRole + customCSS string + enableRSS bool ) - if a.IsRemote() { - // Domain may be in Punycode, - // de-punify it just in case. + switch { + + // Only extra thing we can do for + // remote accounts is properly + // de-punify their domain. + case a.IsRemote(): d, err := util.DePunify(a.Domain) 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 - } else { - // This is a local account, try to - // fetch more info. Skip for instance - // accounts since they have no user. - if !a.IsInstance() { - user, err := c.db.GetUserByAccountID(ctx, a.ID) - if err != nil { - return nil, fmt.Errorf("AccountToAPIAccountPublic: error getting user from database for account id %s: %w", a.ID, err) - } - switch { - case *user.Admin: - role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin} - case *user.Moderator: - role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator} - default: - role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser} - } + // This is a non-instance local + // account; fetch more info using + // the account User. We skip this + // step for instance accounts since + // they have no user. + case !a.IsInstance(): + user, err := c.db.GetUserByAccountID(ctx, a.ID) + if err != nil { + return nil, gtserror.Newf("db error getting user for account %s: %w", a.ID, err) } - acct = a.Username // omit domain + switch { + case *user.Admin: + role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin} + case *user.Moderator: + role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator} + default: + role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser} + } + + 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 @@ -225,8 +258,8 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A Emojis: apiEmojis, Fields: fields, Suspended: !a.SuspendedAt.IsZero(), - CustomCSS: a.CustomCSS, - EnableRSS: *a.EnableRSS, + CustomCSS: customCSS, + EnableRSS: enableRSS, Role: role, } @@ -279,7 +312,7 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel. if !a.IsInstance() { user, err := c.db.GetUserByAccountID(ctx, a.ID) 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 {