From f819229988bb79de709abbfb438831b1e452afd4 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:23:28 +0200 Subject: [PATCH] [chore] make csv export ordering determinate (#3318) --- internal/api/client/exports/exports_test.go | 2 +- internal/typeutils/csv.go | 139 +++++++++++--------- 2 files changed, 75 insertions(+), 66 deletions(-) diff --git a/internal/api/client/exports/exports_test.go b/internal/api/client/exports/exports_test.go index 3759ec6c1..62997af5d 100644 --- a/internal/api/client/exports/exports_test.go +++ b/internal/api/client/exports/exports_test.go @@ -161,8 +161,8 @@ func (suite *ExportsTestSuite) TestExports() { user: suite.testUsers["local_account_1"], account: suite.testAccounts["local_account_1"], expect: `Account address,Show boosts,Notify on new posts,Languages -admin@localhost:8080,true,false, 1happyturtle@localhost:8080,true,false, +admin@localhost:8080,true,false, `, }, // Export Followers. diff --git a/internal/typeutils/csv.go b/internal/typeutils/csv.go index ae080a63f..7211d5c9c 100644 --- a/internal/typeutils/csv.go +++ b/internal/typeutils/csv.go @@ -18,12 +18,13 @@ package typeutils import ( + "cmp" "context" + "slices" "strconv" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" @@ -77,6 +78,8 @@ func (c *Converter) AccountToExportStats( // FollowingToCSV converts a slice of follows into // a slice of CSV-compatible Following records. +// +// Each follow should be populated. func (c *Converter) FollowingToCSV( ctx context.Context, following []*gtsmodel.Follow, @@ -101,24 +104,19 @@ func (c *Converter) FollowingToCSV( thisDomain = config.GetHost() } + // Pre-sort the follows + // by domain and username. + slices.SortFunc( + following, + func(a *gtsmodel.Follow, b *gtsmodel.Follow) int { + aStr := a.TargetAccount.Domain + "/" + a.TargetAccount.Username + bStr := b.TargetAccount.Domain + "/" + b.TargetAccount.Username + return cmp.Compare(aStr, bStr) + }, + ) + // For each item, add a record. for _, follow := range following { - if follow.TargetAccount == nil { - // Retrieve target account. - var err error - follow.TargetAccount, err = c.state.DB.GetAccountByID( - // Barebones is fine here. - gtscontext.SetBarebones(ctx), - follow.TargetAccountID, - ) - if err != nil { - return nil, gtserror.Newf( - "db error getting target account for follow %s: %w", - follow.ID, err, - ) - } - } - domain := follow.TargetAccount.Domain if domain == "" { // Local account, @@ -144,6 +142,8 @@ func (c *Converter) FollowingToCSV( // FollowersToCSV converts a slice of follows into // a slice of CSV-compatible Followers records. +// +// Each follow should be populated. func (c *Converter) FollowersToCSV( ctx context.Context, followers []*gtsmodel.Follow, @@ -165,24 +165,19 @@ func (c *Converter) FollowersToCSV( thisDomain = config.GetHost() } + // Pre-sort the follows + // by domain and username. + slices.SortFunc( + followers, + func(a *gtsmodel.Follow, b *gtsmodel.Follow) int { + aStr := a.Account.Domain + "/" + a.Account.Username + bStr := b.Account.Domain + "/" + b.Account.Username + return cmp.Compare(aStr, bStr) + }, + ) + // For each item, add a record. for _, follow := range followers { - if follow.Account == nil { - // Retrieve account. - var err error - follow.Account, err = c.state.DB.GetAccountByID( - // Barebones is fine here. - gtscontext.SetBarebones(ctx), - follow.AccountID, - ) - if err != nil { - return nil, gtserror.Newf( - "db error getting account for follow %s: %w", - follow.ID, err, - ) - } - } - domain := follow.Account.Domain if domain == "" { // Local account, @@ -218,6 +213,15 @@ func (c *Converter) ListsToCSV( // CSV doesn't use column headers. records := make([][]string, 0) + // Pre-sort the lists + // alphabetically. + slices.SortFunc( + lists, + func(a *gtsmodel.List, b *gtsmodel.List) int { + return cmp.Compare(a.Title, b.Title) + }, + ) + // For each item, add a record. for _, list := range lists { @@ -231,6 +235,17 @@ func (c *Converter) ListsToCSV( return nil, err } + // Pre-sort the follows + // by domain and username. + slices.SortFunc( + follows, + func(a *gtsmodel.Follow, b *gtsmodel.Follow) int { + aStr := a.TargetAccount.Domain + "/" + a.TargetAccount.Username + bStr := b.TargetAccount.Domain + "/" + b.TargetAccount.Username + return cmp.Compare(aStr, bStr) + }, + ) + // Append each follow as CSV record. for _, follow := range follows { var ( @@ -263,6 +278,8 @@ func (c *Converter) ListsToCSV( // BlocksToCSV converts a slice of blocks into // a slice of CSV-compatible blocks records. +// +// Each block should be populated. func (c *Converter) BlocksToCSV( ctx context.Context, blocks []*gtsmodel.Block, @@ -278,24 +295,19 @@ func (c *Converter) BlocksToCSV( // CSV doesn't use column headers. records := make([][]string, 0, len(blocks)) + // Pre-sort the blocks + // by domain and username. + slices.SortFunc( + blocks, + func(a *gtsmodel.Block, b *gtsmodel.Block) int { + aStr := a.TargetAccount.Domain + "/" + a.TargetAccount.Username + bStr := b.TargetAccount.Domain + "/" + b.TargetAccount.Username + return cmp.Compare(aStr, bStr) + }, + ) + // For each item, add a record. for _, block := range blocks { - if block.TargetAccount == nil { - // Retrieve target account. - var err error - block.TargetAccount, err = c.state.DB.GetAccountByID( - // Barebones is fine here. - gtscontext.SetBarebones(ctx), - block.TargetAccountID, - ) - if err != nil { - return nil, gtserror.Newf( - "db error getting target account for block %s: %w", - block.ID, err, - ) - } - } - domain := block.TargetAccount.Domain if domain == "" { // Local account, @@ -315,6 +327,8 @@ func (c *Converter) BlocksToCSV( // MutesToCSV converts a slice of mutes into // a slice of CSV-compatible mute records. +// +// Each mute should be populated. func (c *Converter) MutesToCSV( ctx context.Context, mutes []*gtsmodel.UserMute, @@ -337,24 +351,19 @@ func (c *Converter) MutesToCSV( thisDomain = config.GetHost() } + // Pre-sort the mutes + // by domain and username. + slices.SortFunc( + mutes, + func(a *gtsmodel.UserMute, b *gtsmodel.UserMute) int { + aStr := a.TargetAccount.Domain + "/" + a.TargetAccount.Username + bStr := b.TargetAccount.Domain + "/" + b.TargetAccount.Username + return cmp.Compare(aStr, bStr) + }, + ) + // For each item, add a record. for _, mute := range mutes { - if mute.TargetAccount == nil { - // Retrieve target account. - var err error - mute.TargetAccount, err = c.state.DB.GetAccountByID( - // Barebones is fine here. - gtscontext.SetBarebones(ctx), - mute.TargetAccountID, - ) - if err != nil { - return nil, gtserror.Newf( - "db error getting target account for mute %s: %w", - mute.ID, err, - ) - } - } - domain := mute.TargetAccount.Domain if domain == "" { // Local account,