diff --git a/internal/api/auth/authorize.go b/internal/api/auth/authorize.go index f8978de92..4977ae4f2 100644 --- a/internal/api/auth/authorize.go +++ b/internal/api/auth/authorize.go @@ -75,8 +75,8 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { return } - app := >smodel.Application{} - if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { + app, err := m.db.GetApplicationByClientID(c.Request.Context(), clientID) + if err != nil { m.clearSession(s) safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID) var errWithCode gtserror.WithCode diff --git a/internal/api/auth/callback.go b/internal/api/auth/callback.go index 8871fd2dc..6a6eecd8c 100644 --- a/internal/api/auth/callback.go +++ b/internal/api/auth/callback.go @@ -107,8 +107,8 @@ func (m *Module) CallbackGETHandler(c *gin.Context) { return } - app := >smodel.Application{} - if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { + app, err := m.db.GetApplicationByClientID(c.Request.Context(), sessionClientID) + if err != nil { m.clearSession(s) safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID) var errWithCode gtserror.WithCode diff --git a/internal/cache/gts.go b/internal/cache/gts.go index f120bcf4e..8d7ebcd98 100644 --- a/internal/cache/gts.go +++ b/internal/cache/gts.go @@ -32,6 +32,7 @@ import ( type GTSCaches struct { account *result.Cache[*gtsmodel.Account] accountNote *result.Cache[*gtsmodel.AccountNote] + application *result.Cache[*gtsmodel.Application] block *result.Cache[*gtsmodel.Block] blockIDs *SliceCache[string] boostOfIDs *SliceCache[string] @@ -67,6 +68,7 @@ type GTSCaches struct { func (c *GTSCaches) Init() { c.initAccount() c.initAccountNote() + c.initApplication() c.initBlock() c.initBlockIDs() c.initBoostOfIDs() @@ -117,6 +119,11 @@ func (c *GTSCaches) AccountNote() *result.Cache[*gtsmodel.AccountNote] { return c.accountNote } +// Application provides access to the gtsmodel Application database cache. +func (c *GTSCaches) Application() *result.Cache[*gtsmodel.Application] { + return c.application +} + // 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) initApplication() { + // Calculate maximum cache size. + cap := calculateResultCacheMax( + sizeofApplication(), // model in-mem size. + config.GetCacheApplicationMemRatio(), + ) + log.Infof(nil, "Application cache size = %d", cap) + + c.application = result.New([]result.Lookup{ + {Name: "ID"}, + {Name: "ClientID"}, + }, func(a1 *gtsmodel.Application) *gtsmodel.Application { + a2 := new(gtsmodel.Application) + *a2 = *a1 + return a2 + }, cap) + + c.application.IgnoreErrors(ignoreErrors) +} + func (c *GTSCaches) initBlock() { // Calculate maximum cache size. cap := calculateResultCacheMax( diff --git a/internal/cache/size.go b/internal/cache/size.go index ec7c554c0..34586b0b1 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -155,6 +155,7 @@ func totalOfRatios() float64 { return 0 + config.GetCacheAccountMemRatio() + config.GetCacheAccountNoteMemRatio() + + config.GetCacheApplicationMemRatio() + config.GetCacheBlockMemRatio() + config.GetCacheBlockIDsMemRatio() + config.GetCacheBoostOfIDsMemRatio() + @@ -217,7 +218,7 @@ func sizeofAccount() uintptr { SilencedAt: time.Now(), SuspendedAt: time.Now(), HideCollections: func() *bool { ok := true; return &ok }(), - SuspensionOrigin: "", + SuspensionOrigin: exampleID, EnableRSS: func() *bool { ok := true; return &ok }(), })) } @@ -231,6 +232,20 @@ func sizeofAccountNote() uintptr { })) } +func sizeofApplication() uintptr { + return uintptr(size.Of(>smodel.Application{ + ID: exampleID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: exampleUsername, + Website: exampleURI, + RedirectURI: exampleURI, + ClientID: exampleID, + ClientSecret: exampleID, + Scopes: exampleTextSmall, + })) +} + func sizeofBlock() uintptr { return uintptr(size.Of(>smodel.Block{ ID: exampleID, @@ -500,5 +515,31 @@ func sizeofVisibility() uintptr { } func sizeofUser() uintptr { - return uintptr(size.Of(>smodel.User{})) + return uintptr(size.Of(>smodel.User{ + ID: exampleID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Email: exampleURI, + AccountID: exampleID, + EncryptedPassword: exampleTextSmall, + CurrentSignInAt: time.Now(), + LastSignInAt: time.Now(), + InviteID: exampleID, + ChosenLanguages: []string{"en", "fr", "jp"}, + FilteredLanguages: []string{"en", "fr", "jp"}, + Locale: "en", + CreatedByApplicationID: exampleID, + LastEmailedAt: time.Now(), + ConfirmationToken: exampleTextSmall, + ConfirmationSentAt: time.Now(), + ConfirmedAt: time.Now(), + UnconfirmedEmail: exampleURI, + Moderator: func() *bool { ok := true; return &ok }(), + Admin: func() *bool { ok := true; return &ok }(), + Disabled: func() *bool { ok := true; return &ok }(), + Approved: func() *bool { ok := true; return &ok }(), + ResetPasswordToken: exampleTextSmall, + ResetPasswordSentAt: time.Now(), + ExternalID: exampleID, + })) } diff --git a/internal/config/config.go b/internal/config/config.go index ef79d4e12..5a26222ed 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -178,6 +178,7 @@ type CacheConfiguration struct { MemoryTarget bytesize.Size `name:"memory-target"` AccountMemRatio float64 `name:"account-mem-ratio"` AccountNoteMemRatio float64 `name:"account-note-mem-ratio"` + ApplicationMemRatio float64 `name:"application-mem-ratio"` BlockMemRatio float64 `name:"block-mem-ratio"` BlockIDsMemRatio float64 `name:"block-mem-ratio"` BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 2bc95f6f1..b78362973 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -147,6 +147,7 @@ var Defaults = Configuration{ // be able to make some more sense :D AccountMemRatio: 18, AccountNoteMemRatio: 0.1, + ApplicationMemRatio: 0.1, BlockMemRatio: 3, BlockIDsMemRatio: 3, BoostOfIDsMemRatio: 3, @@ -170,7 +171,7 @@ var Defaults = Configuration{ StatusFaveIDsMemRatio: 3, TagMemRatio: 3, TombstoneMemRatio: 2, - UserMemRatio: 0.1, + UserMemRatio: 0.25, WebfingerMemRatio: 0.1, VisibilityMemRatio: 2, }, diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index 0a299e7d0..03411853f 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -2499,6 +2499,31 @@ func GetCacheAccountNoteMemRatio() float64 { return global.GetCacheAccountNoteMe // SetCacheAccountNoteMemRatio safely sets the value for global configuration 'Cache.AccountNoteMemRatio' field func SetCacheAccountNoteMemRatio(v float64) { global.SetCacheAccountNoteMemRatio(v) } +// GetCacheApplicationMemRatio safely fetches the Configuration value for state's 'Cache.ApplicationMemRatio' field +func (st *ConfigState) GetCacheApplicationMemRatio() (v float64) { + st.mutex.RLock() + v = st.config.Cache.ApplicationMemRatio + st.mutex.RUnlock() + return +} + +// SetCacheApplicationMemRatio safely sets the Configuration value for state's 'Cache.ApplicationMemRatio' field +func (st *ConfigState) SetCacheApplicationMemRatio(v float64) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.Cache.ApplicationMemRatio = v + st.reloadToViper() +} + +// CacheApplicationMemRatioFlag returns the flag name for the 'Cache.ApplicationMemRatio' field +func CacheApplicationMemRatioFlag() string { return "cache-application-mem-ratio" } + +// GetCacheApplicationMemRatio safely fetches the value for global configuration 'Cache.ApplicationMemRatio' field +func GetCacheApplicationMemRatio() float64 { return global.GetCacheApplicationMemRatio() } + +// SetCacheApplicationMemRatio safely sets the value for global configuration 'Cache.ApplicationMemRatio' field +func SetCacheApplicationMemRatio(v float64) { global.SetCacheApplicationMemRatio(v) } + // GetCacheBlockMemRatio safely fetches the Configuration value for state's 'Cache.BlockMemRatio' field func (st *ConfigState) GetCacheBlockMemRatio() (v float64) { st.mutex.RLock() diff --git a/internal/db/application.go b/internal/db/application.go new file mode 100644 index 000000000..34a857d3f --- /dev/null +++ b/internal/db/application.go @@ -0,0 +1,38 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package db + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type Application interface { + // GetApplicationByID fetches the application from the database with corresponding ID value. + GetApplicationByID(ctx context.Context, id string) (*gtsmodel.Application, error) + + // GetApplicationByClientID fetches the application from the database with corresponding client_id value. + GetApplicationByClientID(ctx context.Context, clientID string) (*gtsmodel.Application, error) + + // PutApplication places the new application in the database, erroring on non-unique ID or client_id. + PutApplication(ctx context.Context, app *gtsmodel.Application) error + + // DeleteApplicationByClientID deletes the application with corresponding client_id value from the database. + DeleteApplicationByClientID(ctx context.Context, clientID string) error +} diff --git a/internal/db/bundb/application.go b/internal/db/bundb/application.go new file mode 100644 index 000000000..b53d2c0b0 --- /dev/null +++ b/internal/db/bundb/application.go @@ -0,0 +1,97 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package bundb + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/uptrace/bun" +) + +type applicationDB struct { + db *WrappedDB + state *state.State +} + +func (a *applicationDB) GetApplicationByID(ctx context.Context, id string) (*gtsmodel.Application, error) { + return a.getApplication( + ctx, + "ID", + func(app *gtsmodel.Application) error { + return a.db.NewSelect().Model(app).Where("? = ?", bun.Ident("id"), id).Scan(ctx) + }, + id, + ) +} + +func (a *applicationDB) GetApplicationByClientID(ctx context.Context, clientID string) (*gtsmodel.Application, error) { + return a.getApplication( + ctx, + "ClientID", + func(app *gtsmodel.Application) error { + return a.db.NewSelect().Model(app).Where("? = ?", bun.Ident("client_id"), clientID).Scan(ctx) + }, + clientID, + ) +} + +func (a *applicationDB) getApplication(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Application) error, keyParts ...any) (*gtsmodel.Application, error) { + return a.state.Caches.GTS.Application().Load(lookup, func() (*gtsmodel.Application, error) { + var app gtsmodel.Application + + // Not cached! Perform database query. + if err := dbQuery(&app); err != nil { + return nil, a.db.ProcessError(err) + } + + return &app, nil + }, keyParts...) +} + +func (a *applicationDB) PutApplication(ctx context.Context, app *gtsmodel.Application) error { + return a.state.Caches.GTS.Application().Store(app, func() error { + _, err := a.db.NewInsert().Model(app).Exec(ctx) + return a.db.ProcessError(err) + }) +} + +func (a *applicationDB) DeleteApplicationByClientID(ctx context.Context, clientID string) error { + // Attempt to delete application. + if _, err := a.db.NewDelete(). + Table("applications"). + Where("? = ?", bun.Ident("client_id"), clientID). + Exec(ctx); err != nil { + return a.db.ProcessError(err) + } + + // NOTE about further side effects: + // + // We don't need to handle updating any statuses or users + // (both of which may contain refs to applications), as + // DeleteApplication__() is only ever called during an + // account deletion, which handles deletion of the user + // and all their statuses already. + // + + // Clear application from the cache. + a.state.Caches.GTS.Application().Invalidate("ClientID", clientID) + + return nil +} diff --git a/internal/db/bundb/application_test.go b/internal/db/bundb/application_test.go new file mode 100644 index 000000000..d2ab05ebd --- /dev/null +++ b/internal/db/bundb/application_test.go @@ -0,0 +1,128 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package bundb_test + +import ( + "context" + "errors" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type ApplicationTestSuite struct { + BunDBStandardTestSuite +} + +func (suite *ApplicationTestSuite) TestGetApplicationBy() { + t := suite.T() + + // Create a new context for this test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + // Sentinel error to mark avoiding a test case. + sentinelErr := errors.New("sentinel") + + // isEqual checks if 2 application models are equal. + isEqual := func(a1, a2 gtsmodel.Application) bool { + // Clear database-set fields. + a1.CreatedAt = time.Time{} + a2.CreatedAt = time.Time{} + a1.UpdatedAt = time.Time{} + a2.UpdatedAt = time.Time{} + + return reflect.DeepEqual(a1, a2) + } + + for _, app := range suite.testApplications { + for lookup, dbfunc := range map[string]func() (*gtsmodel.Application, error){ + "id": func() (*gtsmodel.Application, error) { + return suite.db.GetApplicationByID(ctx, app.ID) + }, + + "client_id": func() (*gtsmodel.Application, error) { + return suite.db.GetApplicationByClientID(ctx, app.ClientID) + }, + } { + // Clear database caches. + suite.state.Caches.Init() + + t.Logf("checking database lookup %q", lookup) + + // Perform database function. + checkApp, err := dbfunc() + if err != nil { + if err == sentinelErr { + continue + } + + t.Errorf("error encountered for database lookup %q: %v", lookup, err) + continue + } + + // Check received application data. + if !isEqual(*checkApp, *app) { + t.Errorf("application does not contain expected data: %+v", checkApp) + continue + } + } + } +} + +func (suite *ApplicationTestSuite) TestDeleteApplicationBy() { + t := suite.T() + + // Create a new context for this test. + ctx, cncl := context.WithCancel(context.Background()) + defer cncl() + + for _, app := range suite.testApplications { + for lookup, dbfunc := range map[string]func() error{ + "client_id": func() error { + return suite.db.DeleteApplicationByClientID(ctx, app.ClientID) + }, + } { + // Clear database caches. + suite.state.Caches.Init() + + t.Logf("checking database lookup %q", lookup) + + // Perform database function. + err := dbfunc() + if err != nil { + t.Errorf("error encountered for database lookup %q: %v", lookup, err) + continue + } + + // Ensure this application has been deleted and cache cleared. + if _, err := suite.db.GetApplicationByID(ctx, app.ID); err != db.ErrNoEntries { + t.Errorf("application does not appear to have been deleted %q: %v", lookup, err) + continue + } + } + } +} + +func TestApplicationTestSuite(t *testing.T) { + suite.Run(t, new(ApplicationTestSuite)) +} diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index 8387bb8d1..26b31ff28 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -60,6 +60,7 @@ var registerTables = []interface{}{ type DBService struct { db.Account db.Admin + db.Application db.Basic db.Domain db.Emoji @@ -168,6 +169,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) { db: db, state: state, }, + Application: &applicationDB{ + db: db, + state: state, + }, Basic: &basicDB{ db: db, }, diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index c6091e2c9..311732299 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -37,19 +37,12 @@ type statusDB struct { state *state.State } -func (s *statusDB) newStatusQ(status interface{}) *bun.SelectQuery { - return s.db. - NewSelect(). - Model(status). - Relation("CreatedWithApplication") -} - func (s *statusDB) GetStatusByID(ctx context.Context, id string) (*gtsmodel.Status, error) { return s.getStatus( ctx, "ID", func(status *gtsmodel.Status) error { - return s.newStatusQ(status).Where("? = ?", bun.Ident("status.id"), id).Scan(ctx) + return s.db.NewSelect().Model(status).Where("? = ?", bun.Ident("status.id"), id).Scan(ctx) }, id, ) @@ -78,7 +71,7 @@ func (s *statusDB) GetStatusByURI(ctx context.Context, uri string) (*gtsmodel.St ctx, "URI", func(status *gtsmodel.Status) error { - return s.newStatusQ(status).Where("? = ?", bun.Ident("status.uri"), uri).Scan(ctx) + return s.db.NewSelect().Model(status).Where("? = ?", bun.Ident("status.uri"), uri).Scan(ctx) }, uri, ) @@ -89,7 +82,7 @@ func (s *statusDB) GetStatusByURL(ctx context.Context, url string) (*gtsmodel.St ctx, "URL", func(status *gtsmodel.Status) error { - return s.newStatusQ(status).Where("? = ?", bun.Ident("status.url"), url).Scan(ctx) + return s.db.NewSelect().Model(status).Where("? = ?", bun.Ident("status.url"), url).Scan(ctx) }, url, ) @@ -100,7 +93,7 @@ func (s *statusDB) GetStatusBoost(ctx context.Context, boostOfID string, byAccou ctx, "BoostOfID.AccountID", func(status *gtsmodel.Status) error { - return s.newStatusQ(status). + return s.db.NewSelect().Model(status). Where("status.boost_of_id = ?", boostOfID). Where("status.account_id = ?", byAccountID). @@ -264,6 +257,17 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) } } + if status.CreatedWithApplicationID != "" && status.CreatedWithApplication == nil { + // Populate the status' expected CreatedWithApplication (not always set). + status.CreatedWithApplication, err = s.state.DB.GetApplicationByID( + ctx, // these are already barebones + status.CreatedWithApplicationID, + ) + if err != nil { + errs.Appendf("error populating status application: %w", err) + } + } + return errs.Combine() } diff --git a/internal/db/bundb/user.go b/internal/db/bundb/user.go index 4b38d48fa..9df05596e 100644 --- a/internal/db/bundb/user.go +++ b/internal/db/bundb/user.go @@ -24,6 +24,7 @@ import ( "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/state" "github.com/uptrace/bun" @@ -35,107 +36,125 @@ type userDB struct { } func (u *userDB) GetUserByID(ctx context.Context, id string) (*gtsmodel.User, error) { - return u.state.Caches.GTS.User().Load("ID", func() (*gtsmodel.User, error) { - var user gtsmodel.User + return u.getUser( + ctx, + "ID", + func(user *gtsmodel.User) error { + return u.db.NewSelect().Model(user).Where("? = ?", bun.Ident("id"), id).Scan(ctx) + }, + id, + ) +} - q := u.db. - NewSelect(). - Model(&user). - Relation("Account"). - Where("? = ?", bun.Ident("user.id"), id) +func (u *userDB) GetUsersByIDs(ctx context.Context, ids []string) ([]*gtsmodel.User, error) { + var ( + users = make([]*gtsmodel.User, 0, len(ids)) - if err := q.Scan(ctx); err != nil { - return nil, u.db.ProcessError(err) + // Collect errors instead of + // returning early on any. + errs gtserror.MultiError + ) + + for _, id := range ids { + // Attempt to fetch user from DB. + user, err := u.GetUserByID(ctx, id) + if err != nil { + errs.Appendf("error getting user %s: %w", id, err) + continue } - return &user, nil - }, id) + // Append user to return slice. + users = append(users, user) + } + + return users, errs.Combine() } func (u *userDB) GetUserByAccountID(ctx context.Context, accountID string) (*gtsmodel.User, error) { - return u.state.Caches.GTS.User().Load("AccountID", func() (*gtsmodel.User, error) { - var user gtsmodel.User - - q := u.db. - NewSelect(). - Model(&user). - Relation("Account"). - Where("? = ?", bun.Ident("user.account_id"), accountID) - - if err := q.Scan(ctx); err != nil { - return nil, u.db.ProcessError(err) - } - - return &user, nil - }, accountID) + return u.getUser( + ctx, + "AccountID", + func(user *gtsmodel.User) error { + return u.db.NewSelect().Model(user).Where("? = ?", bun.Ident("account_id"), accountID).Scan(ctx) + }, + accountID, + ) } -func (u *userDB) GetUserByEmailAddress(ctx context.Context, emailAddress string) (*gtsmodel.User, error) { - return u.state.Caches.GTS.User().Load("Email", func() (*gtsmodel.User, error) { - var user gtsmodel.User - - q := u.db. - NewSelect(). - Model(&user). - Relation("Account"). - Where("? = ?", bun.Ident("user.email"), emailAddress) - - if err := q.Scan(ctx); err != nil { - return nil, u.db.ProcessError(err) - } - - return &user, nil - }, emailAddress) +func (u *userDB) GetUserByEmailAddress(ctx context.Context, email string) (*gtsmodel.User, error) { + return u.getUser( + ctx, + "Email", + func(user *gtsmodel.User) error { + return u.db.NewSelect().Model(user).Where("? = ?", bun.Ident("email"), email).Scan(ctx) + }, + email, + ) } func (u *userDB) GetUserByExternalID(ctx context.Context, id string) (*gtsmodel.User, error) { - return u.state.Caches.GTS.User().Load("ExternalID", func() (*gtsmodel.User, error) { - var user gtsmodel.User - - q := u.db. - NewSelect(). - Model(&user). - Relation("Account"). - Where("? = ?", bun.Ident("user.external_id"), id) - - if err := q.Scan(ctx); err != nil { - return nil, u.db.ProcessError(err) - } - - return &user, nil - }, id) + return u.getUser( + ctx, + "ExternalID", + func(user *gtsmodel.User) error { + return u.db.NewSelect().Model(user).Where("? = ?", bun.Ident("external_id"), id).Scan(ctx) + }, + id, + ) } -func (u *userDB) GetUserByConfirmationToken(ctx context.Context, confirmationToken string) (*gtsmodel.User, error) { - return u.state.Caches.GTS.User().Load("ConfirmationToken", func() (*gtsmodel.User, error) { +func (u *userDB) GetUserByConfirmationToken(ctx context.Context, token string) (*gtsmodel.User, error) { + return u.getUser( + ctx, + "ConfirmationToken", + func(user *gtsmodel.User) error { + return u.db.NewSelect().Model(user).Where("? = ?", bun.Ident("confirmation_token"), token).Scan(ctx) + }, + token, + ) +} + +func (u *userDB) getUser(ctx context.Context, lookup string, dbQuery func(*gtsmodel.User) error, keyParts ...any) (*gtsmodel.User, error) { + // Fetch user from database cache with loader callback. + user, err := u.state.Caches.GTS.User().Load(lookup, func() (*gtsmodel.User, error) { var user gtsmodel.User - q := u.db. - NewSelect(). - Model(&user). - Relation("Account"). - Where("? = ?", bun.Ident("user.confirmation_token"), confirmationToken) - - if err := q.Scan(ctx); err != nil { + // Not cached! perform database query. + if err := dbQuery(&user); err != nil { return nil, u.db.ProcessError(err) } return &user, nil - }, confirmationToken) + }, keyParts...) + if err != nil { + return nil, err + } + + // Fetch the related account model for this user. + user.Account, err = u.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + user.AccountID, + ) + if err != nil { + return nil, gtserror.Newf("error populating user account: %w", err) + } + + return user, nil } func (u *userDB) GetAllUsers(ctx context.Context) ([]*gtsmodel.User, error) { - var users []*gtsmodel.User - q := u.db. - NewSelect(). - Model(&users). - Relation("Account") + var userIDs []string - if err := q.Scan(ctx); err != nil { + // Scan all user IDs into slice. + if err := u.db.NewSelect(). + Table("users"). + Column("id"). + Scan(ctx, &userIDs); err != nil { return nil, u.db.ProcessError(err) } - return users, nil + // Transform user IDs into user slice. + return u.GetUsersByIDs(ctx, userIDs) } func (u *userDB) PutUser(ctx context.Context, user *gtsmodel.User) error { diff --git a/internal/db/db.go b/internal/db/db.go index 7c00050ff..567551c73 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -26,6 +26,7 @@ const ( type DB interface { Account Admin + Application Basic Domain Emoji diff --git a/internal/middleware/tokencheck.go b/internal/middleware/tokencheck.go index 1496363af..d2570c3f0 100644 --- a/internal/middleware/tokencheck.go +++ b/internal/middleware/tokencheck.go @@ -22,7 +22,6 @@ import ( "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/oauth2/v4" @@ -125,8 +124,8 @@ func TokenCheck(dbConn db.DB, validateBearerToken func(r *http.Request) (oauth2. log.Tracef(ctx, "authenticated client %s with bearer token, scope is %s", clientID, ti.GetScope()) // fetch app for this token - app := >smodel.Application{} - if err := dbConn.GetWhere(ctx, []db.Where{{Key: "client_id", Value: clientID}}, app); err != nil { + app, err := dbConn.GetApplicationByClientID(ctx, clientID) + if err != nil { if err != db.ErrNoEntries { log.Errorf(ctx, "database error looking for application with clientID %s: %s", clientID, err) return @@ -134,6 +133,7 @@ func TokenCheck(dbConn db.DB, validateBearerToken func(r *http.Request) (oauth2. log.Warnf(ctx, "no app found for client %s", clientID) return } + c.Set(oauth.SessionAuthorizedApplication, app) } } diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index d2483aeb1..da13eb20e 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -46,12 +46,6 @@ func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origi }...) l.Trace("beginning account delete process") - if account.IsLocal() { - if err := p.deleteUserAndTokensForAccount(ctx, account); err != nil { - return gtserror.NewErrorInternalError(err) - } - } - if err := p.deleteAccountFollows(ctx, account); err != nil { return gtserror.NewErrorInternalError(err) } @@ -72,6 +66,14 @@ func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origi return gtserror.NewErrorInternalError(err) } + if account.IsLocal() { + // we tokens, applications and clients for account as one of the last + // stages during deletion, as other database models rely on these. + if err := p.deleteUserAndTokensForAccount(ctx, account); err != nil { + return gtserror.NewErrorInternalError(err) + } + } + // To prevent the account being created again, // stubbify it and update it in the db. // The account will not be deleted, but it @@ -129,7 +131,7 @@ func (p *Processor) deleteUserAndTokensForAccount(ctx context.Context, account * } // Delete any OAuth applications associated with this token. - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "client_id", Value: t.ClientID}}, &[]*gtsmodel.Application{}); err != nil { + if err := p.state.DB.DeleteApplicationByClientID(ctx, t.ClientID); err != nil { return gtserror.Newf("db error deleting application: %w", err) } @@ -305,7 +307,17 @@ func (p *Processor) deleteAccountStatuses(ctx context.Context, account *gtsmodel statusLoop: for { // Page through account's statuses. - statuses, err = p.state.DB.GetAccountStatuses(ctx, account.ID, deleteSelectLimit, false, false, maxID, "", false, false) + statuses, err = p.state.DB.GetAccountStatuses( + ctx, + account.ID, + deleteSelectLimit, + false, + false, + maxID, + "", + false, + false, + ) if err != nil && !errors.Is(err, db.ErrNoEntries) { // Make sure we don't have a real error. return err diff --git a/internal/processing/app.go b/internal/processing/app.go index 07739ce92..d4a923e8a 100644 --- a/internal/processing/app.go +++ b/internal/processing/app.go @@ -61,7 +61,7 @@ func (p *Processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api } // chuck it in the db - if err := p.state.DB.Put(ctx, app); err != nil { + if err := p.state.DB.PutApplication(ctx, app); err != nil { return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 2dc0e4dd5..ab04f6ccc 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -699,8 +699,8 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r } if appID := s.CreatedWithApplicationID; appID != "" { - app := >smodel.Application{} - if err := c.db.GetByID(ctx, appID, app); err != nil { + app, err := c.db.GetApplicationByID(ctx, appID) + if err != nil { return nil, fmt.Errorf("error getting application %s: %w", appID, err) } diff --git a/test/envparsing.sh b/test/envparsing.sh index 59c69e1b5..d9270e7f6 100755 --- a/test/envparsing.sh +++ b/test/envparsing.sh @@ -20,6 +20,7 @@ EXPECT=$(cat << "EOF" "cache": { "account-mem-ratio": 18, "account-note-mem-ratio": 0.1, + "application-mem-ratio": 0.1, "block-mem-ratio": 3, "boost-of-ids-mem-ratio": 3, "emoji-category-mem-ratio": 0.1, @@ -43,7 +44,7 @@ EXPECT=$(cat << "EOF" "status-mem-ratio": 18, "tag-mem-ratio": 3, "tombstone-mem-ratio": 2, - "user-mem-ratio": 0.1, + "user-mem-ratio": 0.25, "visibility-mem-ratio": 2, "webfinger-mem-ratio": 0.1 },