From 33ee61575f2fc15c5a85c3fdbb3823a0cd22d25d Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:39:44 +0100 Subject: [PATCH] [bugfix] Don't copy ptr fields in caches (#2386) --- internal/cache/gts.go | 329 +++++++++++++++++++------ internal/db/bundb/emoji.go | 20 ++ internal/db/bundb/instance.go | 4 +- internal/db/bundb/notification.go | 57 ++++- internal/db/bundb/relationship_note.go | 52 ++-- internal/db/bundb/status_test.go | 45 ++++ internal/db/bundb/user.go | 35 ++- internal/db/emoji.go | 16 ++ internal/db/instance.go | 3 + internal/db/notification.go | 3 + internal/db/relationship.go | 3 + internal/db/user.go | 11 + 12 files changed, 469 insertions(+), 109 deletions(-) diff --git a/internal/cache/gts.go b/internal/cache/gts.go index de49ad127..c19d96444 100644 --- a/internal/cache/gts.go +++ b/internal/cache/gts.go @@ -306,6 +306,20 @@ func (c *GTSCaches) initAccount() { log.Infof(nil, "cache size = %d", cap) + copyF := func(a1 *gtsmodel.Account) *gtsmodel.Account { + a2 := new(gtsmodel.Account) + *a2 = *a1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/account.go. + a2.AvatarMediaAttachment = nil + a2.HeaderMediaAttachment = nil + a2.Emojis = nil + + return a2 + } + c.account = result.New([]result.Lookup{ {Name: "ID"}, {Name: "URI"}, @@ -316,11 +330,7 @@ func (c *GTSCaches) initAccount() { {Name: "OutboxURI"}, {Name: "FollowersURI"}, {Name: "FollowingURI"}, - }, func(a1 *gtsmodel.Account) *gtsmodel.Account { - a2 := new(gtsmodel.Account) - *a2 = *a1 - return a2 - }, cap) + }, copyF, cap) c.account.IgnoreErrors(ignoreErrors) } @@ -334,14 +344,23 @@ func (c *GTSCaches) initAccountNote() { log.Infof(nil, "cache size = %d", cap) + copyF := func(n1 *gtsmodel.AccountNote) *gtsmodel.AccountNote { + n2 := new(gtsmodel.AccountNote) + *n2 = *n1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/relationship_note.go. + n2.Account = nil + n2.TargetAccount = nil + + return n2 + } + c.accountNote = result.New([]result.Lookup{ {Name: "ID"}, {Name: "AccountID.TargetAccountID"}, - }, func(n1 *gtsmodel.AccountNote) *gtsmodel.AccountNote { - n2 := new(gtsmodel.AccountNote) - *n2 = *n1 - return n2 - }, cap) + }, copyF, cap) c.accountNote.IgnoreErrors(ignoreErrors) } @@ -376,17 +395,26 @@ func (c *GTSCaches) initBlock() { log.Infof(nil, "cache size = %d", cap) + copyF := func(b1 *gtsmodel.Block) *gtsmodel.Block { + b2 := new(gtsmodel.Block) + *b2 = *b1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/relationship_block.go. + b2.Account = nil + b2.TargetAccount = nil + + return b2 + } + c.block = result.New([]result.Lookup{ {Name: "ID"}, {Name: "URI"}, {Name: "AccountID.TargetAccountID"}, {Name: "AccountID", Multi: true}, {Name: "TargetAccountID", Multi: true}, - }, func(b1 *gtsmodel.Block) *gtsmodel.Block { - b2 := new(gtsmodel.Block) - *b2 = *b1 - return b2 - }, cap) + }, copyF, cap) c.block.IgnoreErrors(ignoreErrors) } @@ -436,17 +464,25 @@ func (c *GTSCaches) initEmoji() { log.Infof(nil, "cache size = %d", cap) + copyF := func(e1 *gtsmodel.Emoji) *gtsmodel.Emoji { + e2 := new(gtsmodel.Emoji) + *e2 = *e1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/emoji.go. + e2.Category = nil + + return e2 + } + c.emoji = result.New([]result.Lookup{ {Name: "ID"}, {Name: "URI"}, {Name: "Shortcode.Domain", AllowZero: true /* domain can be zero i.e. "" */}, {Name: "ImageStaticURL"}, {Name: "CategoryID", Multi: true}, - }, func(e1 *gtsmodel.Emoji) *gtsmodel.Emoji { - e2 := new(gtsmodel.Emoji) - *e2 = *e1 - return e2 - }, cap) + }, copyF, cap) c.emoji.IgnoreErrors(ignoreErrors) } @@ -481,17 +517,26 @@ func (c *GTSCaches) initFollow() { log.Infof(nil, "cache size = %d", cap) + copyF := func(f1 *gtsmodel.Follow) *gtsmodel.Follow { + f2 := new(gtsmodel.Follow) + *f2 = *f1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/relationship_follow.go. + f2.Account = nil + f2.TargetAccount = nil + + return f2 + } + c.follow = result.New([]result.Lookup{ {Name: "ID"}, {Name: "URI"}, {Name: "AccountID.TargetAccountID"}, {Name: "AccountID", Multi: true}, {Name: "TargetAccountID", Multi: true}, - }, func(f1 *gtsmodel.Follow) *gtsmodel.Follow { - f2 := new(gtsmodel.Follow) - *f2 = *f1 - return f2 - }, cap) + }, copyF, cap) c.follow.IgnoreErrors(ignoreErrors) } @@ -519,17 +564,26 @@ func (c *GTSCaches) initFollowRequest() { log.Infof(nil, "cache size = %d", cap) + copyF := func(f1 *gtsmodel.FollowRequest) *gtsmodel.FollowRequest { + f2 := new(gtsmodel.FollowRequest) + *f2 = *f1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/relationship_follow_req.go. + f2.Account = nil + f2.TargetAccount = nil + + return f2 + } + c.followRequest = result.New([]result.Lookup{ {Name: "ID"}, {Name: "URI"}, {Name: "AccountID.TargetAccountID"}, {Name: "AccountID", Multi: true}, {Name: "TargetAccountID", Multi: true}, - }, func(f1 *gtsmodel.FollowRequest) *gtsmodel.FollowRequest { - f2 := new(gtsmodel.FollowRequest) - *f2 = *f1 - return f2 - }, cap) + }, copyF, cap) c.followRequest.IgnoreErrors(ignoreErrors) } @@ -571,14 +625,23 @@ func (c *GTSCaches) initInstance() { log.Infof(nil, "cache size = %d", cap) + copyF := func(i1 *gtsmodel.Instance) *gtsmodel.Instance { + i2 := new(gtsmodel.Instance) + *i2 = *i1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/instance.go. + i2.DomainBlock = nil + i2.ContactAccount = nil + + return i1 + } + c.instance = result.New([]result.Lookup{ {Name: "ID"}, {Name: "Domain"}, - }, func(i1 *gtsmodel.Instance) *gtsmodel.Instance { - i2 := new(gtsmodel.Instance) - *i2 = *i1 - return i1 - }, cap) + }, copyF, cap) c.instance.IgnoreErrors(ignoreErrors) } @@ -592,13 +655,22 @@ func (c *GTSCaches) initList() { log.Infof(nil, "cache size = %d", cap) - c.list = result.New([]result.Lookup{ - {Name: "ID"}, - }, func(l1 *gtsmodel.List) *gtsmodel.List { + copyF := func(l1 *gtsmodel.List) *gtsmodel.List { l2 := new(gtsmodel.List) *l2 = *l1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/list.go. + l2.Account = nil + l2.ListEntries = nil + return l2 - }, cap) + } + + c.list = result.New([]result.Lookup{ + {Name: "ID"}, + }, copyF, cap) c.list.IgnoreErrors(ignoreErrors) } @@ -612,15 +684,23 @@ func (c *GTSCaches) initListEntry() { log.Infof(nil, "cache size = %d", cap) + copyF := func(l1 *gtsmodel.ListEntry) *gtsmodel.ListEntry { + l2 := new(gtsmodel.ListEntry) + *l2 = *l1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/list.go. + l2.Follow = nil + + return l2 + } + c.listEntry = result.New([]result.Lookup{ {Name: "ID"}, {Name: "ListID", Multi: true}, {Name: "FollowID", Multi: true}, - }, func(l1 *gtsmodel.ListEntry) *gtsmodel.ListEntry { - l2 := new(gtsmodel.ListEntry) - *l2 = *l1 - return l2 - }, cap) + }, copyF, cap) c.listEntry.IgnoreErrors(ignoreErrors) } @@ -674,13 +754,23 @@ func (c *GTSCaches) initMention() { log.Infof(nil, "cache size = %d", cap) - c.mention = result.New([]result.Lookup{ - {Name: "ID"}, - }, func(m1 *gtsmodel.Mention) *gtsmodel.Mention { + copyF := func(m1 *gtsmodel.Mention) *gtsmodel.Mention { m2 := new(gtsmodel.Mention) *m2 = *m1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/mention.go. + m2.Status = nil + m2.OriginAccount = nil + m2.TargetAccount = nil + return m2 - }, cap) + } + + c.mention = result.New([]result.Lookup{ + {Name: "ID"}, + }, copyF, cap) c.mention.IgnoreErrors(ignoreErrors) } @@ -694,14 +784,24 @@ func (c *GTSCaches) initNotification() { log.Infof(nil, "cache size = %d", cap) + copyF := func(n1 *gtsmodel.Notification) *gtsmodel.Notification { + n2 := new(gtsmodel.Notification) + *n2 = *n1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/notification.go. + n2.Status = nil + n2.OriginAccount = nil + n2.TargetAccount = nil + + return n2 + } + c.notification = result.New([]result.Lookup{ {Name: "ID"}, {Name: "NotificationType.TargetAccountID.OriginAccountID.StatusID"}, - }, func(n1 *gtsmodel.Notification) *gtsmodel.Notification { - n2 := new(gtsmodel.Notification) - *n2 = *n1 - return n2 - }, cap) + }, copyF, cap) c.notification.IgnoreErrors(ignoreErrors) } @@ -715,14 +815,22 @@ func (c *GTSCaches) initPoll() { log.Infof(nil, "cache size = %d", cap) + copyF := func(p1 *gtsmodel.Poll) *gtsmodel.Poll { + p2 := new(gtsmodel.Poll) + *p2 = *p1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/poll.go. + p2.Status = nil + + return p2 + } + c.poll = result.New([]result.Lookup{ {Name: "ID"}, {Name: "StatusID"}, - }, func(p1 *gtsmodel.Poll) *gtsmodel.Poll { - p2 := new(gtsmodel.Poll) - *p2 = *p1 - return p2 - }, cap) + }, copyF, cap) c.poll.IgnoreErrors(ignoreErrors) } @@ -736,15 +844,24 @@ func (c *GTSCaches) initPollVote() { log.Infof(nil, "cache size = %d", cap) + copyF := func(v1 *gtsmodel.PollVote) *gtsmodel.PollVote { + v2 := new(gtsmodel.PollVote) + *v2 = *v1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/poll.go. + v2.Account = nil + v2.Poll = nil + + return v2 + } + c.pollVote = result.New([]result.Lookup{ {Name: "ID"}, {Name: "PollID.AccountID"}, {Name: "PollID", Multi: true}, - }, func(v1 *gtsmodel.PollVote) *gtsmodel.PollVote { - v2 := new(gtsmodel.PollVote) - *v2 = *v1 - return v2 - }, cap) + }, copyF, cap) c.pollVote.IgnoreErrors(ignoreErrors) } @@ -772,13 +889,25 @@ func (c *GTSCaches) initReport() { log.Infof(nil, "cache size = %d", cap) - c.report = result.New([]result.Lookup{ - {Name: "ID"}, - }, func(r1 *gtsmodel.Report) *gtsmodel.Report { + copyF := func(r1 *gtsmodel.Report) *gtsmodel.Report { r2 := new(gtsmodel.Report) *r2 = *r1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/report.go. + r2.Account = nil + r2.TargetAccount = nil + r2.Statuses = nil + r2.Rules = nil + r2.ActionTakenByAccount = nil + return r2 - }, cap) + } + + c.report = result.New([]result.Lookup{ + {Name: "ID"}, + }, copyF, cap) c.report.IgnoreErrors(ignoreErrors) } @@ -792,17 +921,35 @@ func (c *GTSCaches) initStatus() { log.Infof(nil, "cache size = %d", cap) + copyF := func(s1 *gtsmodel.Status) *gtsmodel.Status { + s2 := new(gtsmodel.Status) + *s2 = *s1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/status.go. + s2.Account = nil + s2.InReplyTo = nil + s2.InReplyToAccount = nil + s2.BoostOf = nil + s2.BoostOfAccount = nil + s2.Poll = nil + s2.Attachments = nil + s2.Tags = nil + s2.Mentions = nil + s2.Emojis = nil + s2.CreatedWithApplication = nil + + return s2 + } + c.status = result.New([]result.Lookup{ {Name: "ID"}, {Name: "URI"}, {Name: "URL"}, {Name: "BoostOfID.AccountID"}, {Name: "ThreadID", Multi: true}, - }, func(s1 *gtsmodel.Status) *gtsmodel.Status { - s2 := new(gtsmodel.Status) - *s2 = *s1 - return s2 - }, cap) + }, copyF, cap) c.status.IgnoreErrors(ignoreErrors) } @@ -816,15 +963,25 @@ func (c *GTSCaches) initStatusFave() { log.Infof(nil, "cache size = %d", cap) + copyF := func(f1 *gtsmodel.StatusFave) *gtsmodel.StatusFave { + f2 := new(gtsmodel.StatusFave) + *f2 = *f1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/statusfave.go. + f2.Account = nil + f2.TargetAccount = nil + f2.Status = nil + + return f2 + } + c.statusFave = result.New([]result.Lookup{ {Name: "ID"}, {Name: "AccountID.StatusID"}, {Name: "StatusID", Multi: true}, - }, func(f1 *gtsmodel.StatusFave) *gtsmodel.StatusFave { - f2 := new(gtsmodel.StatusFave) - *f2 = *f1 - return f2 - }, cap) + }, copyF, cap) c.statusFave.IgnoreErrors(ignoreErrors) } @@ -916,17 +1073,25 @@ func (c *GTSCaches) initUser() { log.Infof(nil, "cache size = %d", cap) + copyF := func(u1 *gtsmodel.User) *gtsmodel.User { + u2 := new(gtsmodel.User) + *u2 = *u1 + + // Don't include ptr fields that + // will be populated separately. + // See internal/db/bundb/user.go. + u2.Account = nil + + return u2 + } + c.user = result.New([]result.Lookup{ {Name: "ID"}, {Name: "AccountID"}, {Name: "Email"}, {Name: "ConfirmationToken"}, {Name: "ExternalID"}, - }, func(u1 *gtsmodel.User) *gtsmodel.User { - u2 := new(gtsmodel.User) - *u2 = *u1 - return u2 - }, cap) + }, copyF, cap) c.user.IgnoreErrors(ignoreErrors) } diff --git a/internal/db/bundb/emoji.go b/internal/db/bundb/emoji.go index a3a19485d..34a08b694 100644 --- a/internal/db/bundb/emoji.go +++ b/internal/db/bundb/emoji.go @@ -26,6 +26,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/log" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -548,6 +549,25 @@ func (e *emojiDB) getEmoji(ctx context.Context, lookup string, dbQuery func(*gts return emoji, nil } +func (e *emojiDB) PopulateEmoji(ctx context.Context, emoji *gtsmodel.Emoji) error { + var ( + errs = gtserror.NewMultiError(1) + err error + ) + + if emoji.CategoryID != "" && emoji.Category == nil { + emoji.Category, err = e.GetEmojiCategory( + ctx, // these are already barebones + emoji.CategoryID, + ) + if err != nil { + errs.Appendf("error populating emoji category: %w", err) + } + } + + return errs.Combine() +} + func (e *emojiDB) GetEmojisByIDs(ctx context.Context, emojiIDs []string) ([]*gtsmodel.Emoji, error) { if len(emojiIDs) == 0 { return nil, db.ErrNoEntries diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go index 6fec3f2fe..567a44ee2 100644 --- a/internal/db/bundb/instance.go +++ b/internal/db/bundb/instance.go @@ -173,14 +173,14 @@ func (i *instanceDB) getInstance(ctx context.Context, lookup string, dbQuery fun } // Further populate the instance fields where applicable. - if err := i.populateInstance(ctx, instance); err != nil { + if err := i.PopulateInstance(ctx, instance); err != nil { return nil, err } return instance, nil } -func (i *instanceDB) populateInstance(ctx context.Context, instance *gtsmodel.Instance) error { +func (i *instanceDB) PopulateInstance(ctx context.Context, instance *gtsmodel.Instance) error { var ( err error errs = gtserror.NewMultiError(2) diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index 423fd0be1..7532b9993 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -23,6 +23,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/id" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -57,7 +58,7 @@ func (n *notificationDB) GetNotification( originAccountID string, statusID string, ) (*gtsmodel.Notification, error) { - return n.state.Caches.GTS.Notification().Load("NotificationType.TargetAccountID.OriginAccountID.StatusID", func() (*gtsmodel.Notification, error) { + notif, err := n.state.Caches.GTS.Notification().Load("NotificationType.TargetAccountID.OriginAccountID.StatusID", func() (*gtsmodel.Notification, error) { var notif gtsmodel.Notification q := n.db.NewSelect(). @@ -73,6 +74,60 @@ func (n *notificationDB) GetNotification( return ¬if, nil }, notificationType, targetAccountID, originAccountID, statusID) + if err != nil { + return nil, err + } + + if gtscontext.Barebones(ctx) { + // no need to fully populate. + return notif, nil + } + + // Further populate the notif fields where applicable. + if err := n.PopulateNotification(ctx, notif); err != nil { + return nil, err + } + + return notif, nil +} + +func (n *notificationDB) PopulateNotification(ctx context.Context, notif *gtsmodel.Notification) error { + var ( + errs = gtserror.NewMultiError(2) + err error + ) + + if notif.TargetAccount == nil { + notif.TargetAccount, err = n.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + notif.TargetAccountID, + ) + if err != nil { + errs.Appendf("error populating notif target account: %w", err) + } + } + + if notif.OriginAccount == nil { + notif.OriginAccount, err = n.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + notif.OriginAccountID, + ) + if err != nil { + errs.Appendf("error populating notif origin account: %w", err) + } + } + + if notif.StatusID != "" && notif.Status == nil { + notif.Status, err = n.state.DB.GetStatusByID( + gtscontext.SetBarebones(ctx), + notif.StatusID, + ) + if err != nil { + errs.Appendf("error populating notif status: %w", err) + } + } + + return errs.Combine() } func (n *notificationDB) GetAccountNotifications( diff --git a/internal/db/bundb/relationship_note.go b/internal/db/bundb/relationship_note.go index 84f0ebeab..f7d15f8b7 100644 --- a/internal/db/bundb/relationship_note.go +++ b/internal/db/bundb/relationship_note.go @@ -19,10 +19,10 @@ package bundb import ( "context" - "fmt" "time" "github.com/superseriousbusiness/gotosocial/internal/gtscontext" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/uptrace/bun" ) @@ -64,27 +64,45 @@ func (r *relationshipDB) getNote(ctx context.Context, lookup string, dbQuery fun return note, nil } - // Set the note source account - note.Account, err = r.state.DB.GetAccountByID( - gtscontext.SetBarebones(ctx), - note.AccountID, - ) - if err != nil { - return nil, fmt.Errorf("error getting note source account: %w", err) - } - - // Set the note target account - note.TargetAccount, err = r.state.DB.GetAccountByID( - gtscontext.SetBarebones(ctx), - note.TargetAccountID, - ) - if err != nil { - return nil, fmt.Errorf("error getting note target account: %w", err) + // Further populate the account fields where applicable. + if err := r.PopulateNote(ctx, note); err != nil { + return nil, err } return note, nil } +func (r *relationshipDB) PopulateNote(ctx context.Context, note *gtsmodel.AccountNote) error { + var ( + errs = gtserror.NewMultiError(2) + err error + ) + + // Ensure note source account set. + if note.Account == nil { + note.Account, err = r.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + note.AccountID, + ) + if err != nil { + errs.Appendf("error populating note source account: %w", err) + } + } + + // Ensure note target account set. + if note.TargetAccount == nil { + note.TargetAccount, err = r.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + note.TargetAccountID, + ) + if err != nil { + errs.Appendf("error populating note target account: %w", err) + } + } + + return errs.Combine() +} + func (r *relationshipDB) PutNote(ctx context.Context, note *gtsmodel.AccountNote) error { note.UpdatedAt = time.Now() return r.state.Caches.GTS.AccountNote().Store(note, func() error { diff --git a/internal/db/bundb/status_test.go b/internal/db/bundb/status_test.go index c0ff6c0da..2129aa0e8 100644 --- a/internal/db/bundb/status_test.go +++ b/internal/db/bundb/status_test.go @@ -224,6 +224,51 @@ func (suite *StatusTestSuite) TestUpdateStatus() { suite.True(updated.PinnedAt.IsZero()) } +func (suite *StatusTestSuite) TestPutPopulatedStatus() { + ctx := context.Background() + + targetStatus := >smodel.Status{} + *targetStatus = *suite.testStatuses["admin_account_status_1"] + + // Populate fields on the target status. + if err := suite.db.PopulateStatus(ctx, targetStatus); err != nil { + suite.FailNow(err.Error()) + } + + // Delete it from the database. + if err := suite.db.DeleteStatusByID(ctx, targetStatus.ID); err != nil { + suite.FailNow(err.Error()) + } + + // Reinsert the populated version + // so that it becomes cached. + if err := suite.db.PutStatus(ctx, targetStatus); err != nil { + suite.FailNow(err.Error()) + } + + // Update the status owner's + // account with a new bio. + account := >smodel.Account{} + *account = *targetStatus.Account + account.Note = "new note for this test" + if err := suite.db.UpdateAccount(ctx, account, "note"); err != nil { + suite.FailNow(err.Error()) + } + + dbStatus, err := suite.db.GetStatusByID(ctx, targetStatus.ID) + if err != nil { + suite.FailNow(err.Error()) + } + + // Account note should be updated, + // even though we stored this + // status with the old note. + suite.Equal( + "new note for this test", + dbStatus.Account.Note, + ) +} + func TestStatusTestSuite(t *testing.T) { suite.Run(t, new(StatusTestSuite)) } diff --git a/internal/db/bundb/user.go b/internal/db/bundb/user.go index eaa1d8e3d..46b3c568f 100644 --- a/internal/db/bundb/user.go +++ b/internal/db/bundb/user.go @@ -130,18 +130,39 @@ func (u *userDB) getUser(ctx context.Context, lookup string, dbQuery func(*gtsmo 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) + if gtscontext.Barebones(ctx) { + // Return without populating. + return user, nil + } + + if err := u.PopulateUser(ctx, user); err != nil { + return nil, err } return user, nil } +// PopulateUser ensures that the user's struct fields are populated. +func (u *userDB) PopulateUser(ctx context.Context, user *gtsmodel.User) error { + var ( + errs = gtserror.NewMultiError(1) + err error + ) + + if user.Account == nil { + // Fetch the related account model for this user. + user.Account, err = u.state.DB.GetAccountByID( + gtscontext.SetBarebones(ctx), + user.AccountID, + ) + if err != nil { + errs.Appendf("error populating user account: %w", err) + } + } + + return errs.Combine() +} + func (u *userDB) GetAllUsers(ctx context.Context) ([]*gtsmodel.User, error) { var userIDs []string diff --git a/internal/db/emoji.go b/internal/db/emoji.go index c4dabd1aa..fed956933 100644 --- a/internal/db/emoji.go +++ b/internal/db/emoji.go @@ -32,13 +32,17 @@ const EmojiAllDomains string = "all" type Emoji interface { // PutEmoji puts one emoji in the database. PutEmoji(ctx context.Context, emoji *gtsmodel.Emoji) error + // UpdateEmoji updates the given columns of one emoji. // If no columns are specified, every column is updated. UpdateEmoji(ctx context.Context, emoji *gtsmodel.Emoji, columns ...string) error + // DeleteEmojiByID deletes one emoji by its database ID. DeleteEmojiByID(ctx context.Context, id string) error + // GetEmojisByIDs gets emojis for the given IDs. GetEmojisByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Emoji, error) + // GetUseableEmojis gets all emojis which are useable by accounts on this instance. GetUseableEmojis(ctx context.Context) ([]*gtsmodel.Emoji, error) @@ -53,23 +57,35 @@ type Emoji interface { // GetEmojisBy gets emojis based on given parameters. Useful for admin actions. GetEmojisBy(ctx context.Context, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) ([]*gtsmodel.Emoji, error) + // GetEmojiByID gets a specific emoji by its database ID. GetEmojiByID(ctx context.Context, id string) (*gtsmodel.Emoji, error) + + // PopulateEmoji populates the struct pointers on the given emoji. + PopulateEmoji(ctx context.Context, emoji *gtsmodel.Emoji) error + // GetEmojiByShortcodeDomain gets an emoji based on its shortcode and domain. // For local emoji, domain should be an empty string. GetEmojiByShortcodeDomain(ctx context.Context, shortcode string, domain string) (*gtsmodel.Emoji, error) + // GetEmojiByURI returns one emoji based on its ActivityPub URI. GetEmojiByURI(ctx context.Context, uri string) (*gtsmodel.Emoji, error) + // GetEmojiByStaticURL gets an emoji using the URL of the static version of the emoji image. GetEmojiByStaticURL(ctx context.Context, imageStaticURL string) (*gtsmodel.Emoji, error) + // PutEmojiCategory puts one new emoji category in the database. PutEmojiCategory(ctx context.Context, emojiCategory *gtsmodel.EmojiCategory) error + // GetEmojiCategoriesByIDs gets emoji categories for given IDs. GetEmojiCategoriesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.EmojiCategory, error) + // GetEmojiCategories gets a slice of the names of all existing emoji categories. GetEmojiCategories(ctx context.Context) ([]*gtsmodel.EmojiCategory, error) + // GetEmojiCategory gets one emoji category by its id. GetEmojiCategory(ctx context.Context, id string) (*gtsmodel.EmojiCategory, error) + // GetEmojiCategoryByName gets one emoji category by its name. GetEmojiCategoryByName(ctx context.Context, name string) (*gtsmodel.EmojiCategory, error) } diff --git a/internal/db/instance.go b/internal/db/instance.go index 408522b65..3b9588fef 100644 --- a/internal/db/instance.go +++ b/internal/db/instance.go @@ -40,6 +40,9 @@ type Instance interface { // GetInstanceByID returns the instance entry corresponding to the given id, if it exists. GetInstanceByID(ctx context.Context, id string) (*gtsmodel.Instance, error) + // PopulateInstance populates the struct pointers on the given instance. + PopulateInstance(ctx context.Context, instance *gtsmodel.Instance) error + // PutInstance inserts the given instance into the database. PutInstance(ctx context.Context, instance *gtsmodel.Instance) error diff --git a/internal/db/notification.go b/internal/db/notification.go index 5f8766252..ab8b5cc6d 100644 --- a/internal/db/notification.go +++ b/internal/db/notification.go @@ -37,6 +37,9 @@ type Notification interface { // Since not all notifications are about a status, statusID can be an empty string. GetNotification(ctx context.Context, notificationType gtsmodel.NotificationType, targetAccountID string, originAccountID string, statusID string) (*gtsmodel.Notification, error) + // PopulateNotification ensures that the notification's struct fields are populated. + PopulateNotification(ctx context.Context, notif *gtsmodel.Notification) error + // PutNotification will insert the given notification into the database. PutNotification(ctx context.Context, notif *gtsmodel.Notification) error diff --git a/internal/db/relationship.go b/internal/db/relationship.go index b3b45551b..5191701bb 100644 --- a/internal/db/relationship.go +++ b/internal/db/relationship.go @@ -184,4 +184,7 @@ type Relationship interface { // PutNote creates or updates a private note. PutNote(ctx context.Context, note *gtsmodel.AccountNote) error + + // PopulateNote populates the struct pointers on the given note. + PopulateNote(ctx context.Context, note *gtsmodel.AccountNote) error } diff --git a/internal/db/user.go b/internal/db/user.go index 9df672837..c762ef2b3 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -27,20 +27,31 @@ import ( type User interface { // GetAllUsers returns all local user accounts, or an error if something goes wrong. GetAllUsers(ctx context.Context) ([]*gtsmodel.User, error) + // GetUserByID returns one user with the given ID, or an error if something goes wrong. GetUserByID(ctx context.Context, id string) (*gtsmodel.User, error) + // GetUserByAccountID returns one user by its account ID, or an error if something goes wrong. GetUserByAccountID(ctx context.Context, accountID string) (*gtsmodel.User, error) + // GetUserByID returns one user with the given email address, or an error if something goes wrong. GetUserByEmailAddress(ctx context.Context, emailAddress string) (*gtsmodel.User, error) + // GetUserByExternalID returns one user with the given external id, or an error if something goes wrong. GetUserByExternalID(ctx context.Context, id string) (*gtsmodel.User, error) + // GetUserByConfirmationToken returns one user by its confirmation token, or an error if something goes wrong. GetUserByConfirmationToken(ctx context.Context, confirmationToken string) (*gtsmodel.User, error) + + // PopulateUser populates the struct pointers on the given user. + PopulateUser(ctx context.Context, user *gtsmodel.User) error + // PutUser will attempt to place user in the database PutUser(ctx context.Context, user *gtsmodel.User) error + // UpdateUser updates one user by its primary key, updating either only the specified columns, or all of them. UpdateUser(ctx context.Context, user *gtsmodel.User, columns ...string) error + // DeleteUserByID deletes one user by its ID. DeleteUserByID(ctx context.Context, userID string) error }