[chore] Move local account settings to separate db table (#2770)

* [chore] Move local account settings to separate database model

* don't use separate settings_id
This commit is contained in:
tobi 2024-03-22 14:03:46 +01:00 committed by GitHub
parent 0767647056
commit 7f4a0a1aeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 525 additions and 191 deletions

View file

@ -395,12 +395,8 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.EqualValues(requestingAccount.AlsoKnownAsURIs, dbUpdatedAccount.AlsoKnownAsURIs)
suite.EqualValues(requestingAccount.MovedToURI, dbUpdatedAccount.MovedToURI)
suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot)
suite.EqualValues(requestingAccount.Reason, dbUpdatedAccount.Reason)
suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked)
suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable)
suite.EqualValues(requestingAccount.Privacy, dbUpdatedAccount.Privacy)
suite.EqualValues(requestingAccount.Sensitive, dbUpdatedAccount.Sensitive)
suite.EqualValues(requestingAccount.Language, dbUpdatedAccount.Language)
suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI)
suite.EqualValues(requestingAccount.URL, dbUpdatedAccount.URL)
suite.EqualValues(requestingAccount.InboxURI, dbUpdatedAccount.InboxURI)
@ -414,7 +410,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
suite.EqualValues(requestingAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt)
suite.EqualValues(requestingAccount.SilencedAt, dbUpdatedAccount.SilencedAt)
suite.EqualValues(requestingAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt)
suite.EqualValues(requestingAccount.HideCollections, dbUpdatedAccount.HideCollections)
suite.EqualValues(requestingAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin)
}
@ -464,9 +459,7 @@ func (suite *InboxPostTestSuite) TestPostDelete() {
suite.Empty(dbAccount.AvatarRemoteURL)
suite.Empty(dbAccount.HeaderMediaAttachmentID)
suite.Empty(dbAccount.HeaderRemoteURL)
suite.Empty(dbAccount.Reason)
suite.Empty(dbAccount.Fields)
suite.True(*dbAccount.HideCollections)
suite.False(*dbAccount.Discoverable)
suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second)
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)

View file

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

View file

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

View file

@ -103,16 +103,22 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
}
func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {
// set default post language of account 1 to markdown
testAccount := suite.testAccounts["local_account_1"]
testAccount.StatusContentType = "text/markdown"
a := testAccount
// Copy zork.
testAccount := &gtsmodel.Account{}
*testAccount = *suite.testAccounts["local_account_1"]
err := suite.db.UpdateAccount(context.Background(), a)
// Copy zork's settings.
settings := &gtsmodel.AccountSettings{}
*settings = *suite.testAccounts["local_account_1"].Settings
testAccount.Settings = settings
// set default post language of zork to markdown
testAccount.Settings.StatusContentType = "text/markdown"
err := suite.db.UpdateAccountSettings(context.Background(), testAccount.Settings)
if err != nil {
suite.FailNow(err.Error())
}
suite.Equal(a.StatusContentType, "text/markdown")
suite.Equal(testAccount.Settings.StatusContentType, "text/markdown")
t := suite.testTokens["local_account_1"]
oauthToken := oauth.DBTokenToToken(t)
@ -122,7 +128,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, a)
ctx.Set(oauth.SessionAuthorizedAccount, testAccount)
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil)
ctx.Request.Header.Set("accept", "application/json")

View file

@ -100,7 +100,6 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom
targetAccount := &gtsmodel.Account{
ID: "01FG1K8EA7SYHEC7V6XKVNC4ZA",
Username: "new_account_domain_user",
Privacy: gtsmodel.VisibilityDefault,
URI: "http://" + host + "/users/new_account_domain_user",
URL: "http://" + host + "/@new_account_domain_user",
InboxURI: "http://" + host + "/users/new_account_domain_user/inbox",
@ -118,6 +117,10 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom
suite.FailNow(err.Error())
}
if err := suite.db.PutAccountSettings(context.Background(), &gtsmodel.AccountSettings{AccountID: targetAccount.ID}); err != nil {
suite.FailNow(err.Error())
}
return targetAccount
}

View file

@ -53,6 +53,7 @@ func (c *Caches) Init() {
c.initAccount()
c.initAccountCounts()
c.initAccountNote()
c.initAccountSettings()
c.initApplication()
c.initBlock()
c.initBlockIDs()
@ -119,6 +120,7 @@ func (c *Caches) Stop() {
func (c *Caches) Sweep(threshold float64) {
c.GTS.Account.Trim(threshold)
c.GTS.AccountNote.Trim(threshold)
c.GTS.AccountSettings.Trim(threshold)
c.GTS.Block.Trim(threshold)
c.GTS.BlockIDs.Trim(threshold)
c.GTS.Emoji.Trim(threshold)

27
internal/cache/db.go vendored
View file

@ -43,6 +43,9 @@ type GTSCaches struct {
Pinned int
}]
// AccountSettings provides access to the gtsmodel AccountSettings database cache.
AccountSettings structr.Cache[*gtsmodel.AccountSettings]
// Application provides access to the gtsmodel Application database cache.
Application structr.Cache[*gtsmodel.Application]
@ -190,6 +193,7 @@ func (c *Caches) initAccount() {
a2.Emojis = nil
a2.AlsoKnownAs = nil
a2.Move = nil
a2.Settings = nil
return a2
}
@ -262,6 +266,29 @@ func (c *Caches) initAccountNote() {
})
}
func (c *Caches) initAccountSettings() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
sizeofAccountSettings(), // model in-mem size.
config.GetCacheAccountSettingsMemRatio(),
)
log.Infof(nil, "cache size = %d", cap)
c.GTS.AccountSettings.Init(structr.Config[*gtsmodel.AccountSettings]{
Indices: []structr.IndexConfig{
{Fields: "AccountID"},
},
MaxSize: cap,
IgnoreErr: ignoreErrors,
CopyValue: func(s1 *gtsmodel.AccountSettings) *gtsmodel.AccountSettings {
s2 := new(gtsmodel.AccountSettings)
*s2 = *s1
return s2
},
})
}
func (c *Caches) initApplication() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(

View file

@ -28,6 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
const (
@ -219,9 +220,6 @@ func sizeofAccount() uintptr {
Bot: func() *bool { ok := true; return &ok }(),
Locked: func() *bool { ok := true; return &ok }(),
Discoverable: func() *bool { ok := false; return &ok }(),
Privacy: gtsmodel.VisibilityFollowersOnly,
Sensitive: func() *bool { ok := true; return &ok }(),
Language: "fr",
URI: exampleURI,
URL: exampleURI,
InboxURI: exampleURI,
@ -236,9 +234,7 @@ func sizeofAccount() uintptr {
SensitizedAt: exampleTime,
SilencedAt: exampleTime,
SuspendedAt: exampleTime,
HideCollections: func() *bool { ok := true; return &ok }(),
SuspensionOrigin: exampleID,
EnableRSS: func() *bool { ok := true; return &ok }(),
}))
}
@ -251,6 +247,22 @@ func sizeofAccountNote() uintptr {
}))
}
func sizeofAccountSettings() uintptr {
return uintptr(size.Of(&gtsmodel.AccountSettings{
AccountID: exampleID,
CreatedAt: exampleTime,
UpdatedAt: exampleTime,
Reason: exampleText,
Privacy: gtsmodel.VisibilityFollowersOnly,
Sensitive: util.Ptr(true),
Language: "fr",
StatusContentType: "text/plain",
CustomCSS: exampleText,
EnableRSS: util.Ptr(true),
HideCollections: util.Ptr(false),
}))
}
func sizeofApplication() uintptr {
return uintptr(size.Of(&gtsmodel.Application{
ID: exampleID,

View file

@ -195,6 +195,7 @@ type CacheConfiguration struct {
MemoryTarget bytesize.Size `name:"memory-target"`
AccountMemRatio float64 `name:"account-mem-ratio"`
AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
AccountSettingsMemRatio float64 `name:"account-settings-mem-ratio"`
ApplicationMemRatio float64 `name:"application-mem-ratio"`
BlockMemRatio float64 `name:"block-mem-ratio"`
BlockIDsMemRatio float64 `name:"block-mem-ratio"`

View file

@ -159,6 +159,7 @@ var Defaults = Configuration{
// be able to make some more sense :D
AccountMemRatio: 5,
AccountNoteMemRatio: 1,
AccountSettingsMemRatio: 0.1,
ApplicationMemRatio: 0.1,
BlockMemRatio: 2,
BlockIDsMemRatio: 3,

View file

@ -2825,6 +2825,31 @@ func GetCacheAccountNoteMemRatio() float64 { return global.GetCacheAccountNoteMe
// SetCacheAccountNoteMemRatio safely sets the value for global configuration 'Cache.AccountNoteMemRatio' field
func SetCacheAccountNoteMemRatio(v float64) { global.SetCacheAccountNoteMemRatio(v) }
// GetCacheAccountSettingsMemRatio safely fetches the Configuration value for state's 'Cache.AccountSettingsMemRatio' field
func (st *ConfigState) GetCacheAccountSettingsMemRatio() (v float64) {
st.mutex.RLock()
v = st.config.Cache.AccountSettingsMemRatio
st.mutex.RUnlock()
return
}
// SetCacheAccountSettingsMemRatio safely sets the Configuration value for state's 'Cache.AccountSettingsMemRatio' field
func (st *ConfigState) SetCacheAccountSettingsMemRatio(v float64) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.Cache.AccountSettingsMemRatio = v
st.reloadToViper()
}
// CacheAccountSettingsMemRatioFlag returns the flag name for the 'Cache.AccountSettingsMemRatio' field
func CacheAccountSettingsMemRatioFlag() string { return "cache-account-settings-mem-ratio" }
// GetCacheAccountSettingsMemRatio safely fetches the value for global configuration 'Cache.AccountSettingsMemRatio' field
func GetCacheAccountSettingsMemRatio() float64 { return global.GetCacheAccountSettingsMemRatio() }
// SetCacheAccountSettingsMemRatio safely sets the value for global configuration 'Cache.AccountSettingsMemRatio' field
func SetCacheAccountSettingsMemRatio(v float64) { global.SetCacheAccountSettingsMemRatio(v) }
// GetCacheApplicationMemRatio safely fetches the Configuration value for state's 'Cache.ApplicationMemRatio' field
func (st *ConfigState) GetCacheApplicationMemRatio() (v float64) {
st.mutex.RLock()

View file

@ -117,4 +117,13 @@ type Account interface {
// GetInstanceAccount returns the instance account for the given domain.
// If domain is empty, this instance account will be returned.
GetInstanceAccount(ctx context.Context, domain string) (*gtsmodel.Account, error)
// Get local account settings with the given ID.
GetAccountSettings(ctx context.Context, id string) (*gtsmodel.AccountSettings, error)
// Store local account settings.
PutAccountSettings(ctx context.Context, settings *gtsmodel.AccountSettings) error
// Update local account settings.
UpdateAccountSettings(ctx context.Context, settings *gtsmodel.AccountSettings, columns ...string) error
}

View file

@ -338,6 +338,17 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
}
}
if account.IsLocal() && account.Settings == nil && !account.IsInstance() {
// Account settings not set, fetch from db.
account.Settings, err = a.state.DB.GetAccountSettings(
ctx, // these are already barebones
account.ID,
)
if err != nil {
errs.Appendf("error populating account settings: %w", err)
}
}
return errs.Combine()
}
@ -504,12 +515,22 @@ func (a *accountDB) SetAccountHeaderOrAvatar(ctx context.Context, mediaAttachmen
}
func (a *accountDB) GetAccountCustomCSSByUsername(ctx context.Context, username string) (string, error) {
// Get local account.
account, err := a.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
return "", err
}
return account.CustomCSS, nil
// Ensure settings populated, in case
// barebones context was passed.
if account.Settings == nil {
account.Settings, err = a.GetAccountSettings(ctx, account.ID)
if err != nil {
return "", err
}
}
return account.Settings.CustomCSS, nil
}
func (a *accountDB) GetAccountsUsingEmoji(ctx context.Context, emojiID string) ([]*gtsmodel.Account, error) {
@ -780,3 +801,68 @@ func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string,
return a.state.DB.GetStatusesByIDs(ctx, statusIDs)
}
func (a *accountDB) GetAccountSettings(
ctx context.Context,
accountID string,
) (*gtsmodel.AccountSettings, error) {
// Fetch settings from db cache with loader callback.
return a.state.Caches.GTS.AccountSettings.LoadOne(
"AccountID",
func() (*gtsmodel.AccountSettings, error) {
// Not cached! Perform database query.
var settings gtsmodel.AccountSettings
if err := a.db.
NewSelect().
Model(&settings).
Where("? = ?", bun.Ident("account_settings.account_id"), accountID).
Scan(ctx); err != nil {
return nil, err
}
return &settings, nil
},
accountID,
)
}
func (a *accountDB) PutAccountSettings(
ctx context.Context,
settings *gtsmodel.AccountSettings,
) error {
return a.state.Caches.GTS.AccountSettings.Store(settings, func() error {
if _, err := a.db.
NewInsert().
Model(settings).
Exec(ctx); err != nil {
return err
}
return nil
})
}
func (a *accountDB) UpdateAccountSettings(
ctx context.Context,
settings *gtsmodel.AccountSettings,
columns ...string,
) error {
return a.state.Caches.GTS.AccountSettings.Store(settings, func() error {
settings.UpdatedAt = time.Now()
if len(columns) > 0 {
// If we're updating by column,
// ensure "updated_at" is included.
columns = append(columns, "updated_at")
}
if _, err := a.db.
NewUpdate().
Model(settings).
Column(columns...).
Where("? = ?", bun.Ident("account_settings.account_id"), settings.AccountID).
Exec(ctx); err != nil {
return err
}
return nil
})
}

View file

@ -216,6 +216,8 @@ func (suite *AccountTestSuite) TestGetAccountBy() {
a2.AvatarMediaAttachment = nil
a1.Emojis = nil
a2.Emojis = nil
a1.Settings = nil
a2.Settings = nil
// Clear database-set fields.
a1.CreatedAt = time.Time{}
@ -439,15 +441,11 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
err = suite.db.Put(context.Background(), newAccount)
suite.NoError(err)
suite.Equal("en", newAccount.Language)
suite.WithinDuration(time.Now(), newAccount.CreatedAt, 30*time.Second)
suite.WithinDuration(time.Now(), newAccount.UpdatedAt, 30*time.Second)
suite.True(*newAccount.Locked)
suite.False(*newAccount.Memorial)
suite.False(*newAccount.Bot)
suite.False(*newAccount.Discoverable)
suite.False(*newAccount.Sensitive)
suite.False(*newAccount.HideCollections)
}
func (suite *AccountTestSuite) TestGetAccountPinnedStatusesSomeResults() {

View file

@ -119,12 +119,21 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
return nil, err
}
settings := &gtsmodel.AccountSettings{
AccountID: accountID,
Reason: newSignup.Reason,
Privacy: gtsmodel.VisibilityDefault,
}
// Insert the settings!
if err := a.state.DB.PutAccountSettings(ctx, settings); err != nil {
return nil, err
}
account = &gtsmodel.Account{
ID: accountID,
Username: newSignup.Username,
DisplayName: newSignup.Username,
Reason: newSignup.Reason,
Privacy: gtsmodel.VisibilityDefault,
URI: uris.UserURI,
URL: uris.UserURL,
InboxURI: uris.InboxURI,
@ -136,6 +145,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
PrivateKey: privKey,
PublicKey: &privKey.PublicKey,
PublicKeyURI: uris.PublicKeyURI,
Settings: settings,
}
// Insert the new account!

View file

@ -85,19 +85,13 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
suite.Nil(a.Fields)
suite.Empty(a.Note)
suite.Empty(a.NoteRaw)
suite.False(*a.Memorial)
suite.Empty(a.AlsoKnownAsURIs)
suite.Empty(a.MovedToURI)
suite.False(*a.Bot)
suite.Empty(a.Reason)
// Locked is especially important, since it's a bool that defaults
// to true, which is why we use pointers for bools in the first place
suite.True(*a.Locked)
suite.False(*a.Discoverable)
suite.Empty(a.Privacy)
suite.False(*a.Sensitive)
suite.Equal("en", a.Language)
suite.Empty(a.StatusContentType)
suite.Equal(testAccount.URI, a.URI)
suite.Equal(testAccount.URL, a.URL)
suite.Zero(testAccount.FetchedAt)
@ -113,7 +107,6 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() {
suite.Zero(a.SensitizedAt)
suite.Zero(a.SilencedAt)
suite.Zero(a.SuspendedAt)
suite.False(*a.HideCollections)
suite.Empty(a.SuspensionOrigin)
}

View file

@ -0,0 +1,122 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
oldgtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20230328203024_migration_fix"
newgtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
log.Info(ctx, "migrating account settings to new table, please wait...")
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Columns we'll be moving
// to AccountSettings.
var columns = []string{
"reason",
"privacy",
"sensitive",
"language",
"status_content_type",
"custom_css",
"enable_rss",
"hide_collections",
}
// Create the new account settings table.
if _, err := tx.
NewCreateTable().
Model(&newgtsmodel.AccountSettings{}).
IfNotExists().
Exec(ctx); err != nil {
return err
}
// Select each local account.
accounts := []*oldgtsmodel.Account{}
if err := tx.
NewSelect().
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
Column("account.id").
Column(columns...).
Join(
"JOIN ? AS ? ON ? = ?",
bun.Ident("users"), bun.Ident("user"),
bun.Ident("user.account_id"), bun.Ident("account.id"),
).
Scan(ctx, &accounts); err != nil {
return err
}
// Create a settings entry for each existing account, taking
// values from the old account model (with sensible defaults).
for _, account := range accounts {
settings := &newgtsmodel.AccountSettings{
AccountID: account.ID,
CreatedAt: account.CreatedAt,
Reason: account.Reason,
Privacy: newgtsmodel.Visibility(account.Privacy),
Sensitive: util.Ptr(util.PtrValueOr(account.Sensitive, false)),
Language: account.Language,
StatusContentType: account.StatusContentType,
CustomCSS: account.CustomCSS,
EnableRSS: util.Ptr(util.PtrValueOr(account.EnableRSS, false)),
HideCollections: util.Ptr(util.PtrValueOr(account.HideCollections, false)),
}
// Insert the settings model.
if _, err := tx.
NewInsert().
Model(settings).
Exec(ctx); err != nil {
return err
}
}
// Drop now unused columns from accounts table.
for _, column := range columns {
if _, err := tx.
NewDropColumn().
Table("accounts").
Column(column).
Exec(ctx); err != nil {
return err
}
}
return nil
})
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -743,9 +743,6 @@ func (d *Dereferencer) enrichAccount(
// Set time of update from the last-fetched date.
latestAcc.UpdatedAt = latestAcc.FetchedAt
// Carry over existing account language.
latestAcc.Language = account.Language
// This is an existing account, update the model in the database.
if err := d.state.DB.UpdateAccount(ctx, latestAcc); err != nil {
return nil, nil, gtserror.Newf("error updating database: %w", err)

View file

@ -48,45 +48,38 @@ type Account struct {
DisplayName string `bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this account's bio, display name, etc
Emojis []*Emoji `bun:"attached_emojis,m2m:account_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
Fields []*Field // A slice of of fields that this account has added to their profile.
FieldsRaw []*Field // The raw (unparsed) content of fields that this account has added to their profile, without conversion to HTML, only available when requester = target
Note string `bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves)
NoteRaw string `bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target
Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away?
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs.
AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db).
MovedToURI string `bun:",nullzero"` // This account has (or claims to have) moved to this account URI. Even if this field is set the move may not yet have been processed. Check `move` for this.
MovedTo *Account `bun:"-"` // This account has moved to this account (field not stored in the db).
MoveID string `bun:"type:CHAR(26),nullzero"` // ID of a Move in the database for this account. Only set if we received or created a Move activity for which this account URI was the origin.
Move *Move `bun:"-"` // Move corresponding to MoveID, if set.
Bot *bool `bun:",default:false"` // Does this account identify itself as a bot?
Reason string `bun:""` // What reason was given for signing up when this account was created?
Locked *bool `bun:",default:true"` // Does this account need an approval for new followers?
Discoverable *bool `bun:",default:false"` // Should this account be shown in the instance's profile directory?
Privacy Visibility `bun:",nullzero"` // Default post privacy for this account
Sensitive *bool `bun:",default:false"` // Set posts from this account to sensitive by default?
Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts).
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI for this account.
URL string `bun:",nullzero,unique"` // Web URL for this account's profile
InboxURI string `bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to
SharedInboxURI *string `bun:""` // Address of this account's ActivityPub sharedInbox. Gotcha warning: this is a string pointer because it has three possible states: 1. We don't know yet if the account has a shared inbox -- null. 2. We know it doesn't have a shared inbox -- empty string. 3. We know it does have a shared inbox -- url string.
OutboxURI string `bun:",nullzero,unique"` // Address of this account's activitypub outbox
FollowingURI string `bun:",nullzero,unique"` // URI for getting the following list of this account
FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account
FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account
ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account?
PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts
PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for authorizing signed activitypub requests, will be defined for both local and remote accounts
PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // PublicKey will expire/has expired at given time, and should be fetched again as appropriate. Only ever set for remote accounts.
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive?
SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
HideCollections *bool `bun:",default:false"` // Hide this account's collections
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"` // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID
EnableRSS *bool `bun:",default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
Fields []*Field `bun:""` // A slice of of fields that this account has added to their profile.
FieldsRaw []*Field `bun:""` // The raw (unparsed) content of fields that this account has added to their profile, without conversion to HTML, only available when requester = target
Note string `bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves)
NoteRaw string `bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target
Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away?
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs.
AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db).
MovedToURI string `bun:",nullzero"` // This account has (or claims to have) moved to this account URI. Even if this field is set the move may not yet have been processed. Check `move` for this.
MovedTo *Account `bun:"-"` // This account has moved to this account (field not stored in the db).
MoveID string `bun:"type:CHAR(26),nullzero"` // ID of a Move in the database for this account. Only set if we received or created a Move activity for which this account URI was the origin.
Move *Move `bun:"-"` // Move corresponding to MoveID, if set.
Bot *bool `bun:",default:false"` // Does this account identify itself as a bot?
Locked *bool `bun:",default:true"` // Does this account need an approval for new followers?
Discoverable *bool `bun:",default:false"` // Should this account be shown in the instance's profile directory?
URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI for this account.
URL string `bun:",nullzero,unique"` // Web URL for this account's profile
InboxURI string `bun:",nullzero,unique"` // Address of this account's ActivityPub inbox, for sending activity to
SharedInboxURI *string `bun:""` // Address of this account's ActivityPub sharedInbox. Gotcha warning: this is a string pointer because it has three possible states: 1. We don't know yet if the account has a shared inbox -- null. 2. We know it doesn't have a shared inbox -- empty string. 3. We know it does have a shared inbox -- url string.
OutboxURI string `bun:",nullzero,unique"` // Address of this account's activitypub outbox
FollowingURI string `bun:",nullzero,unique"` // URI for getting the following list of this account
FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account
FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account
ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account?
PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts
PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for authorizing signed activitypub requests, will be defined for both local and remote accounts
PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // PublicKey will expire/has expired at given time, and should be fetched again as appropriate. Only ever set for remote accounts.
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive?
SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
SuspensionOrigin string `bun:"type:CHAR(26),nullzero"` // id of the database entry that caused this account to become suspended -- can be an account ID or a domain block ID
Settings *AccountSettings `bun:"-"` // gtsmodel.AccountSettings for this account.
}
// IsLocal returns whether account is a local user account.

View file

@ -0,0 +1,35 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package gtsmodel
import "time"
// AccountSettings models settings / preferences for a local, non-instance account.
type AccountSettings struct {
AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings.
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created.
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated.
Reason string `bun:",nullzero"` // What reason was given for signing up when this account was created?
Privacy Visibility `bun:",nullzero"` // Default post privacy for this account
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default?
Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts).
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
}

View file

@ -518,14 +518,9 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
account.Memorial = util.Ptr(false)
account.AlsoKnownAsURIs = nil
account.MovedToURI = ""
account.Reason = ""
account.Discoverable = util.Ptr(false)
account.StatusContentType = ""
account.CustomCSS = ""
account.SuspendedAt = now
account.SuspensionOrigin = origin
account.HideCollections = util.Ptr(true)
account.EnableRSS = util.Ptr(false)
return []string{
"fetched_at",
@ -541,14 +536,9 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
"memorial",
"also_known_as_uris",
"moved_to_uri",
"reason",
"discoverable",
"status_content_type",
"custom_css",
"suspended_at",
"suspension_origin",
"hide_collections",
"enable_rss",
}
}

View file

@ -66,14 +66,9 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
suite.Zero(updatedAccount.NoteRaw)
suite.False(*updatedAccount.Memorial)
suite.Empty(updatedAccount.AlsoKnownAsURIs)
suite.Zero(updatedAccount.Reason)
suite.False(*updatedAccount.Discoverable)
suite.Zero(updatedAccount.StatusContentType)
suite.Zero(updatedAccount.CustomCSS)
suite.WithinDuration(time.Now(), updatedAccount.SuspendedAt, 1*time.Minute)
suite.Equal(suspensionOrigin, updatedAccount.SuspensionOrigin)
suite.True(*updatedAccount.HideCollections)
suite.False(*updatedAccount.EnableRSS)
updatedUser, err := suite.db.GetUserByAccountID(ctx, testAccount.ID)
if err != nil {

View file

@ -64,7 +64,7 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
}
// Ensure account has rss feed enabled.
if !*account.EnableRSS {
if !*account.Settings.EnableRSS {
err = gtserror.New("account RSS feed not enabled")
return nil, never, gtserror.NewErrorNotFound(err)
}

View file

@ -47,6 +47,11 @@ func (p *Processor) selectNoteFormatter(contentType string) text.FormatFunc {
// Update processes the update of an account with the given form.
func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
// Ensure account populated; we'll need settings.
if err := p.state.DB.PopulateAccount(ctx, account); err != nil {
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
}
if form.Discoverable != nil {
account.Discoverable = form.Discoverable
}
@ -146,7 +151,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
// Format + set note according to user prefs.
f := p.selectNoteFormatter(account.StatusContentType)
f := p.selectNoteFormatter(account.Settings.StatusContentType)
formatNoteResult := f(ctx, p.parseMention, account.ID, "", account.NoteRaw)
account.Note = formatNoteResult.HTML
@ -227,11 +232,11 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
if err != nil {
return nil, gtserror.NewErrorBadRequest(err)
}
account.Language = language
account.Settings.Language = language
}
if form.Source.Sensitive != nil {
account.Sensitive = form.Source.Sensitive
account.Settings.Sensitive = form.Source.Sensitive
}
if form.Source.Privacy != nil {
@ -239,7 +244,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
return nil, gtserror.NewErrorBadRequest(err)
}
privacy := typeutils.APIVisToVis(apimodel.Visibility(*form.Source.Privacy))
account.Privacy = privacy
account.Settings.Privacy = privacy
}
if form.Source.StatusContentType != nil {
@ -247,7 +252,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
account.StatusContentType = *form.Source.StatusContentType
account.Settings.StatusContentType = *form.Source.StatusContentType
}
}
@ -256,18 +261,21 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
if err := validate.CustomCSS(customCSS); err != nil {
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
account.CustomCSS = text.SanitizeToPlaintext(customCSS)
account.Settings.CustomCSS = text.SanitizeToPlaintext(customCSS)
}
if form.EnableRSS != nil {
account.EnableRSS = form.EnableRSS
account.Settings.EnableRSS = form.EnableRSS
}
err := p.state.DB.UpdateAccount(ctx, account)
if err != nil {
if err := p.state.DB.UpdateAccount(ctx, account); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err))
}
if err := p.state.DB.UpdateAccountSettings(ctx, account.Settings); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account settings %s: %s", account.ID, err))
}
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityUpdate,

View file

@ -126,9 +126,15 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() {
}
func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
// Copy zork.
testAccount := &gtsmodel.Account{}
*testAccount = *suite.testAccounts["local_account_1"]
// Copy zork's settings.
settings := &gtsmodel.AccountSettings{}
*settings = *suite.testAccounts["local_account_1"].Settings
testAccount.Settings = settings
var (
ctx = context.Background()
note = "*hello* ~~here~~ i am!"
@ -136,8 +142,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
)
// Set status content type of account 1 to markdown for this test.
testAccount.StatusContentType = "text/markdown"
if err := suite.db.UpdateAccount(ctx, testAccount, "status_content_type"); err != nil {
testAccount.Settings.StatusContentType = "text/markdown"
if err := suite.db.UpdateAccountSettings(ctx, testAccount.Settings, "status_content_type"); err != nil {
suite.FailNow(err.Error())
}

View file

@ -32,9 +32,9 @@ func (p *Processor) PreferencesGet(ctx context.Context, accountID string) (*apim
}
return &apimodel.Preferences{
PostingDefaultVisibility: mastoPrefVisibility(act.Privacy),
PostingDefaultSensitive: *act.Sensitive,
PostingDefaultLanguage: act.Language,
PostingDefaultVisibility: mastoPrefVisibility(act.Settings.Privacy),
PostingDefaultSensitive: *act.Settings.Sensitive,
PostingDefaultLanguage: act.Settings.Language,
// The Reading* preferences don't appear to actually be settable by the
// client, so forcing some sensible defaults here
ReadingExpandMedia: "default",

View file

@ -50,6 +50,11 @@ func (p *Processor) Create(
*apimodel.Status,
gtserror.WithCode,
) {
// Ensure account populated; we'll need settings.
if err := p.state.DB.PopulateAccount(ctx, requester); err != nil {
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
}
// Generate new ID for status.
statusID := id.NewULID()
@ -112,11 +117,11 @@ func (p *Processor) Create(
return nil, errWithCode
}
if err := processVisibility(form, requester.Privacy, status); err != nil {
if err := processVisibility(form, requester.Settings.Privacy, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if err := processLanguage(form, requester.Language, status); err != nil {
if err := processLanguage(form, requester.Settings.Language, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@ -369,7 +374,7 @@ func processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLang
func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, status *gtsmodel.Status) error {
if form.ContentType == "" {
// If content type wasn't specified, use the author's preferred content-type.
contentType := apimodel.StatusContentType(status.Account.StatusContentType)
contentType := apimodel.StatusContentType(status.Account.Settings.StatusContentType)
form.ContentType = contentType
}

View file

@ -362,9 +362,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
suite.Empty(dbAccount.AvatarRemoteURL)
suite.Empty(dbAccount.HeaderMediaAttachmentID)
suite.Empty(dbAccount.HeaderRemoteURL)
suite.Empty(dbAccount.Reason)
suite.Empty(dbAccount.Fields)
suite.True(*dbAccount.HideCollections)
suite.False(*dbAccount.Discoverable)
suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second)
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)

View file

@ -25,6 +25,7 @@ import (
"io"
"os"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/log"
transmodel "github.com/superseriousbusiness/gotosocial/internal/trans/model"
)
@ -73,6 +74,14 @@ func (i *importer) inputEntry(ctx context.Context, entry transmodel.Entry) error
if err := i.putInDB(ctx, account); err != nil {
return fmt.Errorf("inputEntry: error adding account to database: %s", err)
}
if account.Domain == "" && account.Username != config.GetHost() {
// Local, non-instance account.
// Insert barebones settings model.
settings := &transmodel.AccountSettings{AccountID: account.ID}
if err := i.putInDB(ctx, settings); err != nil {
return fmt.Errorf("inputEntry: error adding account settings to database: %s", err)
}
}
log.Infof(ctx, "added account with id %s", account.ID)
return nil
case transmodel.TransBlock:

View file

@ -106,11 +106,6 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() {
suite.Equal(testAccountBefore.Memorial, testAccountAfter.Memorial)
suite.Equal(testAccountBefore.Bot, testAccountAfter.Bot)
suite.Equal(testAccountBefore.Locked, testAccountAfter.Locked)
suite.Equal(testAccountBefore.Reason, testAccountAfter.Reason)
suite.Equal(testAccountBefore.Privacy, testAccountAfter.Privacy)
suite.Equal(testAccountBefore.Sensitive, testAccountAfter.Sensitive)
suite.Equal(testAccountBefore.Language, testAccountAfter.Language)
suite.Equal(testAccountBefore.StatusContentType, testAccountAfter.StatusContentType)
suite.Equal(testAccountBefore.URI, testAccountAfter.URI)
suite.Equal(testAccountBefore.URL, testAccountAfter.URL)
suite.Equal(testAccountBefore.InboxURI, testAccountAfter.InboxURI)
@ -123,7 +118,6 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() {
suite.Equal(testAccountBefore.PublicKey, testAccountAfter.PublicKey)
suite.Equal(testAccountBefore.PublicKeyURI, testAccountAfter.PublicKeyURI)
suite.Equal(testAccountBefore.SuspendedAt, testAccountAfter.SuspendedAt)
suite.Equal(testAccountBefore.HideCollections, testAccountAfter.HideCollections)
suite.Equal(testAccountBefore.SuspensionOrigin, testAccountAfter.SuspensionOrigin)
}

View file

@ -36,13 +36,8 @@ type Account struct {
NoteRaw string `json:"noteRaw,omitempty" bun:",nullzero"`
Memorial *bool `json:"memorial"`
Bot *bool `json:"bot"`
Reason string `json:"reason,omitempty" bun:",nullzero"`
Locked *bool `json:"locked"`
Discoverable *bool `json:"discoverable"`
Privacy string `json:"privacy,omitempty" bun:",nullzero"`
Sensitive *bool `json:"sensitive"`
Language string `json:"language,omitempty" bun:",nullzero"`
StatusContentType string `json:"statusContentType,omitempty" bun:",nullzero"`
URI string `json:"uri" bun:",nullzero"`
URL string `json:"url" bun:",nullzero"`
InboxURI string `json:"inboxURI" bun:",nullzero"`
@ -59,6 +54,9 @@ type Account struct {
SensitizedAt *time.Time `json:"sensitizedAt,omitempty" bun:",nullzero"`
SilencedAt *time.Time `json:"silencedAt,omitempty" bun:",nullzero"`
SuspendedAt *time.Time `json:"suspendedAt,omitempty" bun:",nullzero"`
HideCollections *bool `json:"hideCollections"`
SuspensionOrigin string `json:"suspensionOrigin,omitempty" bun:",nullzero"`
}
type AccountSettings struct {
AccountID string
}

View file

@ -130,13 +130,8 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a
// Extract account note (bio / summary).
acct.Note = ap.ExtractSummary(accountable)
// Assume:
// - memorial (TODO)
// - sensitive (TODO)
// - hide collections (TODO)
// Assume not memorial (todo)
acct.Memorial = util.Ptr(false)
acct.Sensitive = util.Ptr(false)
acct.HideCollections = util.Ptr(false)
// Extract 'manuallyApprovesFollowers' aka locked account (default = true).
manuallyApprovesFollowers := ap.GetManuallyApprovesFollowers(accountable)
@ -146,9 +141,6 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a
discoverable := ap.GetDiscoverable(accountable)
acct.Discoverable = &discoverable
// Assume not an RSS feed.
acct.EnableRSS = util.Ptr(false)
// Extract the URL property.
urls := ap.GetURL(accountable)
if len(urls) == 0 {

View file

@ -78,14 +78,14 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
}
statusContentType := string(apimodel.StatusContentTypeDefault)
if a.StatusContentType != "" {
statusContentType = a.StatusContentType
if a.Settings.StatusContentType != "" {
statusContentType = a.Settings.StatusContentType
}
apiAccount.Source = &apimodel.Source{
Privacy: c.VisToAPIVis(ctx, a.Privacy),
Sensitive: *a.Sensitive,
Language: a.Language,
Privacy: c.VisToAPIVis(ctx, a.Settings.Privacy),
Sensitive: *a.Settings.Sensitive,
Language: a.Settings.Language,
StatusContentType: statusContentType,
Note: a.NoteRaw,
Fields: c.fieldsToAPIFields(a.FieldsRaw),
@ -170,10 +170,13 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
// Bits that vary between remote + local accounts:
// - Account (acct) string.
// - Role.
// - Settings things (enableRSS, customCSS).
var (
acct string
role *apimodel.AccountRole
acct string
role *apimodel.AccountRole
enableRSS bool
customCSS string
)
if a.IsRemote() {
@ -203,6 +206,9 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
default:
role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser}
}
enableRSS = *a.Settings.EnableRSS
customCSS = a.Settings.CustomCSS
}
acct = a.Username // omit domain
@ -239,7 +245,6 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
locked = boolPtrDef("locked", a.Locked, true)
discoverable = boolPtrDef("discoverable", a.Discoverable, false)
bot = boolPtrDef("bot", a.Bot, false)
enableRSS = boolPtrDef("enableRSS", a.EnableRSS, false)
)
// Remaining properties are simple and
@ -267,7 +272,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
Emojis: apiEmojis,
Fields: fields,
Suspended: !a.SuspendedAt.IsZero(),
CustomCSS: a.CustomCSS,
CustomCSS: customCSS,
EnableRSS: enableRSS,
Role: role,
Moved: moved,
@ -376,6 +381,10 @@ func (c *Converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
createdByApplicationID string
)
if err := c.state.DB.PopulateAccount(ctx, a); err != nil {
log.Errorf(ctx, "error(s) populating account, will continue: %s", err)
}
if a.IsRemote() {
// Domain may be in Punycode,
// de-punify it just in case.
@ -404,8 +413,8 @@ func (c *Converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
}
locale = user.Locale
if user.Account.Reason != "" {
inviteRequest = &user.Account.Reason
if a.Settings.Reason != "" {
inviteRequest = &a.Settings.Reason
}
if *user.Admin {

View file

@ -26,6 +26,7 @@ EXPECT=$(cat << "EOF"
"cache": {
"account-mem-ratio": 5,
"account-note-mem-ratio": 1,
"account-settings-mem-ratio": 0.1,
"application-mem-ratio": 0.1,
"block-mem-ratio": 3,
"boost-of-ids-mem-ratio": 3,

View file

@ -70,6 +70,7 @@ var testModels = []interface{}{
&gtsmodel.Report{},
&gtsmodel.Rule{},
&gtsmodel.AccountNote{},
&gtsmodel.AccountSettings{},
}
// NewTestDB returns a new initialized, empty database for testing.
@ -206,6 +207,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
}
}
for _, v := range NewTestAccountSettings() {
if err := db.Put(ctx, v); err != nil {
log.Panic(nil, err)
}
}
for _, v := range NewTestAttachments() {
if err := db.Put(ctx, v); err != nil {
log.Panic(nil, err)

View file

@ -286,6 +286,8 @@ func NewTestUsers() map[string]*gtsmodel.User {
// NewTestAccounts returns a map of accounts keyed by what type of account they are.
func NewTestAccounts() map[string]*gtsmodel.Account {
settings := NewTestAccountSettings()
accounts := map[string]*gtsmodel.Account{
"instance_account": {
ID: "01AY6P665V14JJR0AFVRT7311Y",
@ -301,12 +303,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
CreatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
UpdatedAt: TimeMustParse("2020-05-17T13:10:59Z"),
Bot: util.Ptr(false),
Reason: "",
Locked: util.Ptr(false),
Discoverable: util.Ptr(true),
Privacy: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
URI: "http://localhost:8080/users/localhost:8080",
URL: "http://localhost:8080/@localhost:8080",
PublicKeyURI: "http://localhost:8080/users/localhost:8080#main-key",
@ -322,9 +320,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
EnableRSS: util.Ptr(false),
},
"unconfirmed_account": {
ID: "01F8MH0BBE4FHXPH513MBVFHB0",
@ -339,12 +335,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.",
Locked: util.Ptr(false),
Discoverable: util.Ptr(false),
Privacy: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
URI: "http://localhost:8080/users/weed_lord420",
URL: "http://localhost:8080/@weed_lord420",
FetchedAt: time.Time{},
@ -360,9 +352,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
EnableRSS: util.Ptr(false),
Settings: settings["unconfirmed_account"],
},
"admin_account": {
ID: "01F8MH17FWEB39HZJ76B6VXSKF",
@ -378,12 +369,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
Bot: util.Ptr(false),
Reason: "",
Locked: util.Ptr(false),
Discoverable: util.Ptr(true),
Privacy: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
URI: "http://localhost:8080/users/admin",
URL: "http://localhost:8080/@admin",
PublicKeyURI: "http://localhost:8080/users/admin#main-key",
@ -399,9 +386,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
EnableRSS: util.Ptr(true),
Settings: settings["admin_account"],
},
"local_account_1": {
ID: "01F8MH1H7YV1Z7D2C8K2730QBF",
@ -417,12 +403,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
Bot: util.Ptr(false),
Reason: "I wanna be on this damned webbed site so bad! Please! Wow",
Locked: util.Ptr(false),
Discoverable: util.Ptr(true),
Privacy: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
URI: "http://localhost:8080/users/the_mighty_zork",
URL: "http://localhost:8080/@the_mighty_zork",
FetchedAt: time.Time{},
@ -438,9 +420,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
EnableRSS: util.Ptr(true),
Settings: settings["local_account_1"],
},
"local_account_2": {
ID: "01F8MH5NBDF2MV7CTC4Q5128HF",
@ -475,12 +456,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Bot: util.Ptr(false),
Reason: "",
Locked: util.Ptr(true),
Discoverable: util.Ptr(false),
Privacy: gtsmodel.VisibilityFollowersOnly,
Sensitive: util.Ptr(true),
Language: "fr",
URI: "http://localhost:8080/users/1happyturtle",
URL: "http://localhost:8080/@1happyturtle",
FetchedAt: time.Time{},
@ -496,9 +473,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
EnableRSS: util.Ptr(false),
Settings: settings["local_account_2"],
},
"remote_account_1": {
ID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
@ -514,8 +490,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Bot: util.Ptr(false),
Locked: util.Ptr(false),
Discoverable: util.Ptr(true),
Sensitive: util.Ptr(false),
Language: "en",
URI: "http://fossbros-anonymous.io/users/foss_satan",
URL: "http://fossbros-anonymous.io/@foss_satan",
FetchedAt: time.Time{},
@ -532,9 +506,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
EnableRSS: util.Ptr(false),
},
"remote_account_2": {
ID: "01FHMQX3GAABWSM0S2VZEC2SWC",
@ -550,8 +522,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Bot: util.Ptr(false),
Locked: util.Ptr(true),
Discoverable: util.Ptr(true),
Sensitive: util.Ptr(false),
Language: "en",
URI: "http://example.org/users/Some_User",
URL: "http://example.org/@Some_User",
FetchedAt: time.Time{},
@ -568,9 +538,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
EnableRSS: util.Ptr(false),
},
"remote_account_3": {
ID: "062G5WYKY35KKD12EMSM3F8PJ8",
@ -586,8 +554,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Bot: util.Ptr(false),
Locked: util.Ptr(true),
Discoverable: util.Ptr(true),
Sensitive: util.Ptr(false),
Language: "en",
URI: "http://thequeenisstillalive.technology/users/her_fuckin_maj",
URL: "http://thequeenisstillalive.technology/@her_fuckin_maj",
FetchedAt: time.Time{},
@ -604,10 +570,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R",
EnableRSS: util.Ptr(false),
},
"remote_account_4": {
ID: "07GZRBAEMBNKGZ8Z9VSKSXKR98",
@ -622,8 +586,6 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Bot: util.Ptr(false),
Locked: util.Ptr(false),
Discoverable: util.Ptr(false),
Sensitive: util.Ptr(false),
Language: "de",
URI: "https://xn--xample-ova.org/users/%C3%BCser",
URL: "https://xn--xample-ova.org/users/@%C3%BCser",
FetchedAt: time.Time{},
@ -640,10 +602,8 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
SensitizedAt: time.Time{},
SilencedAt: time.Time{},
SuspendedAt: time.Time{},
HideCollections: util.Ptr(false),
SuspensionOrigin: "",
HeaderMediaAttachmentID: "",
EnableRSS: util.Ptr(false),
},
}
@ -698,6 +658,55 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
return accounts
}
func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
return map[string]*gtsmodel.AccountSettings{
"unconfirmed_account": {
AccountID: "01F8MH0BBE4FHXPH513MBVFHB0",
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.",
Privacy: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
EnableRSS: util.Ptr(false),
HideCollections: util.Ptr(false),
},
"admin_account": {
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
Reason: "",
Privacy: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
EnableRSS: util.Ptr(true),
HideCollections: util.Ptr(false),
},
"local_account_1": {
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
Reason: "I wanna be on this damned webbed site so bad! Please! Wow",
Privacy: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
EnableRSS: util.Ptr(true),
HideCollections: util.Ptr(false),
},
"local_account_2": {
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
Reason: "",
Privacy: gtsmodel.VisibilityFollowersOnly,
Sensitive: util.Ptr(true),
Language: "fr",
EnableRSS: util.Ptr(false),
HideCollections: util.Ptr(false),
},
}
}
func NewTestTombstones() map[string]*gtsmodel.Tombstone {
return map[string]*gtsmodel.Tombstone{
"https://somewhere.mysterious/users/rest_in_piss#main-key": {