mirror of
1
Fork 0

[chore] make csv export ordering determinate (#3318)

This commit is contained in:
tobi 2024-09-18 12:23:28 +02:00 committed by GitHub
parent 2f56455eed
commit f819229988
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 75 additions and 66 deletions

View File

@ -161,8 +161,8 @@ func (suite *ExportsTestSuite) TestExports() {
user: suite.testUsers["local_account_1"], user: suite.testUsers["local_account_1"],
account: suite.testAccounts["local_account_1"], account: suite.testAccounts["local_account_1"],
expect: `Account address,Show boosts,Notify on new posts,Languages expect: `Account address,Show boosts,Notify on new posts,Languages
admin@localhost:8080,true,false,
1happyturtle@localhost:8080,true,false, 1happyturtle@localhost:8080,true,false,
admin@localhost:8080,true,false,
`, `,
}, },
// Export Followers. // Export Followers.

View File

@ -18,12 +18,13 @@
package typeutils package typeutils
import ( import (
"cmp"
"context" "context"
"slices"
"strconv" "strconv"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
@ -77,6 +78,8 @@ func (c *Converter) AccountToExportStats(
// FollowingToCSV converts a slice of follows into // FollowingToCSV converts a slice of follows into
// a slice of CSV-compatible Following records. // a slice of CSV-compatible Following records.
//
// Each follow should be populated.
func (c *Converter) FollowingToCSV( func (c *Converter) FollowingToCSV(
ctx context.Context, ctx context.Context,
following []*gtsmodel.Follow, following []*gtsmodel.Follow,
@ -101,24 +104,19 @@ func (c *Converter) FollowingToCSV(
thisDomain = config.GetHost() 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 each item, add a record.
for _, follow := range following { 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 domain := follow.TargetAccount.Domain
if domain == "" { if domain == "" {
// Local account, // Local account,
@ -144,6 +142,8 @@ func (c *Converter) FollowingToCSV(
// FollowersToCSV converts a slice of follows into // FollowersToCSV converts a slice of follows into
// a slice of CSV-compatible Followers records. // a slice of CSV-compatible Followers records.
//
// Each follow should be populated.
func (c *Converter) FollowersToCSV( func (c *Converter) FollowersToCSV(
ctx context.Context, ctx context.Context,
followers []*gtsmodel.Follow, followers []*gtsmodel.Follow,
@ -165,24 +165,19 @@ func (c *Converter) FollowersToCSV(
thisDomain = config.GetHost() 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 each item, add a record.
for _, follow := range followers { 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 domain := follow.Account.Domain
if domain == "" { if domain == "" {
// Local account, // Local account,
@ -218,6 +213,15 @@ func (c *Converter) ListsToCSV(
// CSV doesn't use column headers. // CSV doesn't use column headers.
records := make([][]string, 0) 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 each item, add a record.
for _, list := range lists { for _, list := range lists {
@ -231,6 +235,17 @@ func (c *Converter) ListsToCSV(
return nil, err 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. // Append each follow as CSV record.
for _, follow := range follows { for _, follow := range follows {
var ( var (
@ -263,6 +278,8 @@ func (c *Converter) ListsToCSV(
// BlocksToCSV converts a slice of blocks into // BlocksToCSV converts a slice of blocks into
// a slice of CSV-compatible blocks records. // a slice of CSV-compatible blocks records.
//
// Each block should be populated.
func (c *Converter) BlocksToCSV( func (c *Converter) BlocksToCSV(
ctx context.Context, ctx context.Context,
blocks []*gtsmodel.Block, blocks []*gtsmodel.Block,
@ -278,24 +295,19 @@ func (c *Converter) BlocksToCSV(
// CSV doesn't use column headers. // CSV doesn't use column headers.
records := make([][]string, 0, len(blocks)) 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 each item, add a record.
for _, block := range blocks { 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 domain := block.TargetAccount.Domain
if domain == "" { if domain == "" {
// Local account, // Local account,
@ -315,6 +327,8 @@ func (c *Converter) BlocksToCSV(
// MutesToCSV converts a slice of mutes into // MutesToCSV converts a slice of mutes into
// a slice of CSV-compatible mute records. // a slice of CSV-compatible mute records.
//
// Each mute should be populated.
func (c *Converter) MutesToCSV( func (c *Converter) MutesToCSV(
ctx context.Context, ctx context.Context,
mutes []*gtsmodel.UserMute, mutes []*gtsmodel.UserMute,
@ -337,24 +351,19 @@ func (c *Converter) MutesToCSV(
thisDomain = config.GetHost() 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 each item, add a record.
for _, mute := range mutes { 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 domain := mute.TargetAccount.Domain
if domain == "" { if domain == "" {
// Local account, // Local account,