[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.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)

View File

@ -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() {

View File

@ -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)
}

View File

@ -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)

81
internal/cache/gts.go vendored
View File

@ -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(

View File

@ -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(&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 {
return uintptr(size.Of(&gtsmodel.Block{
ID: exampleID,

View File

@ -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).

View File

@ -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{

View File

@ -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()

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(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)

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()
}
@ -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) {

View File

@ -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() {

View File

@ -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 = &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.
user, err := a.state.DB.GetUserByAccountID(ctx, account.ID)
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
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)
}

View File

@ -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.

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
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 {

View File

@ -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 {