diff --git a/.golangci.yml b/.golangci.yml index 5cae966..06f8c2a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,11 +6,17 @@ linters: - copyloopvar - durationcheck - errcheck + - errchkjson - errorlint - govet + - intrange - ineffassign - misspell + - modernize - nilerr + - nilnesserr + - nolintlint + - perfsprint - rowserrcheck - sloglint - sqlclosecheck @@ -18,6 +24,7 @@ linters: - testifylint - unconvert - unused + - usestdlibvars - wastedassign formatters: diff --git a/CHANGELOG.md b/CHANGELOG.md index 053acae..f4f7f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to `discrawl` will be documented in this file. - `dms` now lists local wiretap DM conversations and can read or search one DM thread with `--with`, `--last`, and `--search`, so common DM queries no longer require raw SQL. - `search --dm` and `messages --dm` now target the local-only `@me` archive directly and skip Git snapshot auto-update, since DMs are never imported from the shared mirror. +- Go module dependencies and lint rules were refreshed for the current Go toolchain, including stricter JSON marshal checks and modern simplification rules. ### Fixes diff --git a/go.mod b/go.mod index 3ce3d90..6007d3a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/pelletier/go-toml/v2 v2.3.0 github.com/stretchr/testify v1.11.1 - golang.org/x/text v0.35.0 + golang.org/x/text v0.36.0 modernc.org/sqlite v1.49.1 ) @@ -15,14 +15,14 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.72.0 // indirect + modernc.org/libc v1.72.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 6d0f4c6..23bc723 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= @@ -26,32 +26,31 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= -modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= -modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= -modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= +modernc.org/cc/v4 v4.28.1 h1:XpLbkYVQ24E8tX5u8+yWGvaxerxkR/S4zqxI8ZoSBuc= +modernc.org/cc/v4 v4.28.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.33.0 h1:dspBCm75jsj8Y/ufwAMVfe375L2iYdMyQ2QG/v3hL54= +modernc.org/ccgo/v4 v4.33.0/go.mod h1:+RhXBoRYzRwaH21mV/aj6XvQRDtfjcZfAlPMsQo8CR0= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= @@ -60,14 +59,14 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= -modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= diff --git a/internal/cli/admin_commands.go b/internal/cli/admin_commands.go index f9ba5ca..fb4087f 100644 --- a/internal/cli/admin_commands.go +++ b/internal/cli/admin_commands.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "flag" "fmt" "io" @@ -250,10 +251,10 @@ func (r *runtime) runWiretap(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("wiretap takes flags only")) + return usageErr(errors.New("wiretap takes flags only")) } if *maxFileBytes <= 0 { - return usageErr(fmt.Errorf("--max-file-bytes must be positive")) + return usageErr(errors.New("--max-file-bytes must be positive")) } runOnce := func(ctx context.Context) error { stats, err := discorddesktop.Import(ctx, r.store, discorddesktop.Options{ @@ -271,7 +272,7 @@ func (r *runtime) runWiretap(args []string) error { return runOnce(r.ctx) } if *watchEvery < time.Second { - return usageErr(fmt.Errorf("--watch-every must be at least 1s")) + return usageErr(errors.New("--watch-every must be at least 1s")) } ctx, stop := signal.NotifyContext(r.ctx, os.Interrupt, syscall.SIGTERM) defer stop() @@ -294,7 +295,7 @@ func (r *runtime) runWiretap(args []string) error { func (r *runtime) runStatus(args []string) error { if len(args) != 0 { - return usageErr(fmt.Errorf("status takes no arguments")) + return usageErr(errors.New("status takes no arguments")) } dbPath, err := config.ExpandPath(r.cfg.DBPath) if err != nil { @@ -317,16 +318,16 @@ func (r *runtime) runEmbed(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("embed takes no positional arguments")) + return usageErr(errors.New("embed takes no positional arguments")) } if *limit <= 0 { - return usageErr(fmt.Errorf("--limit must be positive")) + return usageErr(errors.New("--limit must be positive")) } if *batchSize <= 0 { - return usageErr(fmt.Errorf("--batch-size must be positive")) + return usageErr(errors.New("--batch-size must be positive")) } if !r.cfg.Search.Embeddings.Enabled { - return usageErr(fmt.Errorf("embeddings are disabled in config")) + return usageErr(errors.New("embeddings are disabled in config")) } providerFactory := r.newEmbed if providerFactory == nil { @@ -364,7 +365,7 @@ func (r *runtime) runEmbed(args []string) error { func (r *runtime) runDoctor(args []string) error { if len(args) != 0 { - return usageErr(fmt.Errorf("doctor takes no arguments")) + return usageErr(errors.New("doctor takes no arguments")) } report := map[string]any{ "config_path": r.configPath, diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 5500347..6e814bd 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -1365,7 +1365,7 @@ func TestRuntimeConfiguresAttachmentTextOnSyncer(t *testing.T) { require.NoError(t, rt.withServices(true, func() error { return nil })) require.True(t, fakeSync.attachmentTextEnabled) - cfg.Sync.AttachmentText = ptrBool(false) + cfg.Sync.AttachmentText = new(false) require.NoError(t, config.Write(cfgPath, cfg)) require.NoError(t, rt.withServices(true, func() error { return nil })) require.False(t, fakeSync.attachmentTextEnabled) @@ -1498,10 +1498,6 @@ func discardLogger() *slog.Logger { return slog.New(slog.DiscardHandler) } -func ptrBool(value bool) *bool { - return &value -} - func TestRuntimeHelpersAndSubcommands(t *testing.T) { ctx := context.Background() dir := t.TempDir() diff --git a/internal/cli/direct_messages.go b/internal/cli/direct_messages.go index 570cd2f..6d44ae8 100644 --- a/internal/cli/direct_messages.go +++ b/internal/cli/direct_messages.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "flag" "fmt" "io" @@ -30,28 +31,28 @@ func (r *runtime) runDirectMessages(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("dms takes flags only")) + return usageErr(errors.New("dms takes flags only")) } if *hours < 0 { - return usageErr(fmt.Errorf("--hours must be >= 0")) + return usageErr(errors.New("--hours must be >= 0")) } if *days < 0 { - return usageErr(fmt.Errorf("--days must be >= 0")) + return usageErr(errors.New("--days must be >= 0")) } if countNonZero(*hours > 0, *days > 0, strings.TrimSpace(*since) != "") > 1 { - return usageErr(fmt.Errorf("use only one of --hours, --days, or --since")) + return usageErr(errors.New("use only one of --hours, --days, or --since")) } if *limit < 0 { - return usageErr(fmt.Errorf("--limit must be >= 0")) + return usageErr(errors.New("--limit must be >= 0")) } if *last < 0 { - return usageErr(fmt.Errorf("--last must be >= 0")) + return usageErr(errors.New("--last must be >= 0")) } if *all && *last > 0 && flagPassed(fs, "last") { - return usageErr(fmt.Errorf("use either --all or --last")) + return usageErr(errors.New("use either --all or --last")) } if flagPassed(fs, "limit") && flagPassed(fs, "last") { - return usageErr(fmt.Errorf("use either --limit or --last")) + return usageErr(errors.New("use either --limit or --last")) } if *list || (strings.TrimSpace(*with) == "" && strings.TrimSpace(*search) == "" && noDMMessageTimeFilter(*hours, *days, *since, *before)) { diff --git a/internal/cli/helpers.go b/internal/cli/helpers.go index bab5c6f..2cdf8c1 100644 --- a/internal/cli/helpers.go +++ b/internal/cli/helpers.go @@ -1,8 +1,8 @@ package cli import ( + "errors" "flag" - "fmt" "strings" "time" @@ -26,7 +26,7 @@ func (r *runtime) resolveSyncGuildsAll(guild, guilds string, all bool) ([]string return r.resolveSyncGuilds(guild, guilds), nil } if len(csvList(guilds)) > 0 || strings.TrimSpace(guild) != "" { - return nil, fmt.Errorf("use either --all or --guild/--guilds") + return nil, errors.New("use either --all or --guild/--guilds") } return nil, nil } @@ -42,7 +42,7 @@ func directMessageGuildScope(dm bool, guild, guilds string) ([]string, error) { return csvList(strings.Join(requested, ",")), nil } if len(csvList(guilds)) > 0 || strings.TrimSpace(guild) != "" { - return nil, fmt.Errorf("use either --dm or --guild/--guilds") + return nil, errors.New("use either --dm or --guild/--guilds") } return []string{store.DirectMessageGuildID}, nil } diff --git a/internal/cli/mentions.go b/internal/cli/mentions.go index 7d55704..b742e25 100644 --- a/internal/cli/mentions.go +++ b/internal/cli/mentions.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "flag" "fmt" "io" @@ -27,19 +28,19 @@ func (r *runtime) runMentions(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("mentions takes flags only")) + return usageErr(errors.New("mentions takes flags only")) } if *days < 0 { - return usageErr(fmt.Errorf("--days must be >= 0")) + return usageErr(errors.New("--days must be >= 0")) } if *days > 0 && strings.TrimSpace(*since) != "" { - return usageErr(fmt.Errorf("use either --days or --since")) + return usageErr(errors.New("use either --days or --since")) } if *limit < 0 { - return usageErr(fmt.Errorf("--limit must be >= 0")) + return usageErr(errors.New("--limit must be >= 0")) } if targetTypeValue := strings.TrimSpace(*targetType); targetTypeValue != "" && targetTypeValue != "user" && targetTypeValue != "role" { - return usageErr(fmt.Errorf("--type must be user or role")) + return usageErr(errors.New("--type must be user or role")) } var sinceTime time.Time @@ -73,7 +74,7 @@ func (r *runtime) runMentions(args []string) error { sinceTime.IsZero() && beforeTime.IsZero() && len(guildIDs) == 0 { - return usageErr(fmt.Errorf("mentions needs at least one filter")) + return usageErr(errors.New("mentions needs at least one filter")) } rows, err := r.store.ListMentions(r.ctx, store.MentionListOptions{ diff --git a/internal/cli/messages.go b/internal/cli/messages.go index 8bfb655..49b536a 100644 --- a/internal/cli/messages.go +++ b/internal/cli/messages.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "flag" "fmt" "io" @@ -33,29 +34,29 @@ func (r *runtime) runMessages(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("messages takes flags only")) + return usageErr(errors.New("messages takes flags only")) } if *hours < 0 { - return usageErr(fmt.Errorf("--hours must be >= 0")) + return usageErr(errors.New("--hours must be >= 0")) } if *days < 0 { - return usageErr(fmt.Errorf("--days must be >= 0")) + return usageErr(errors.New("--days must be >= 0")) } if countNonZero(*hours > 0, *days > 0, strings.TrimSpace(*since) != "") > 1 { - return usageErr(fmt.Errorf("use only one of --hours, --days, or --since")) + return usageErr(errors.New("use only one of --hours, --days, or --since")) } if *limit < 0 { - return usageErr(fmt.Errorf("--limit must be >= 0")) + return usageErr(errors.New("--limit must be >= 0")) } if *last < 0 { - return usageErr(fmt.Errorf("--last must be >= 0")) + return usageErr(errors.New("--last must be >= 0")) } limitSet := flagPassed(fs, "limit") if *all && *last > 0 { - return usageErr(fmt.Errorf("use either --all or --last")) + return usageErr(errors.New("use either --all or --last")) } if limitSet && *last > 0 { - return usageErr(fmt.Errorf("use either --limit or --last")) + return usageErr(errors.New("use either --limit or --last")) } if *last > 0 { *limit = 0 @@ -96,10 +97,10 @@ func (r *runtime) runMessages(args []string) error { return usageErr(err) } if *dm && *syncNow { - return usageErr(fmt.Errorf("messages --sync is not supported with --dm; run wiretap or sync --source wiretap first")) + return usageErr(errors.New("messages --sync is not supported with --dm; run wiretap or sync --source wiretap first")) } if strings.TrimSpace(*channel) == "" && strings.TrimSpace(*author) == "" && sinceTime.IsZero() && beforeTime.IsZero() && len(guildIDs) == 0 { - return usageErr(fmt.Errorf("messages needs at least one filter")) + return usageErr(errors.New("messages needs at least one filter")) } if *all { *limit = 0 diff --git a/internal/cli/output.go b/internal/cli/output.go index 3971efe..92d6e0c 100644 --- a/internal/cli/output.go +++ b/internal/cli/output.go @@ -2,6 +2,7 @@ package cli import ( "encoding/json" + "errors" "fmt" "io" "sort" @@ -69,7 +70,7 @@ func printPlain(w io.Writer, value any) error { } return nil default: - return fmt.Errorf("no plain printer") + return errors.New("no plain printer") } } @@ -286,7 +287,7 @@ func printHuman(w io.Writer, value any) error { } return nil default: - return fmt.Errorf("no human printer") + return errors.New("no human printer") } } diff --git a/internal/cli/query_commands.go b/internal/cli/query_commands.go index 26a12ee..f2d6706 100644 --- a/internal/cli/query_commands.go +++ b/internal/cli/query_commands.go @@ -29,7 +29,7 @@ func (r *runtime) runSearch(args []string) error { return usageErr(err) } if fs.NArg() != 1 { - return usageErr(fmt.Errorf("search requires a query")) + return usageErr(errors.New("search requires a query")) } guildIDs, err := directMessageGuildScope(*dm, *guildFlag, *guildsFlag) if err != nil { @@ -107,7 +107,7 @@ func (r *runtime) searchMessagesHybrid(opts store.SearchOptions) ([]store.Search func (r *runtime) semanticSearchOptions(opts store.SearchOptions) (store.SemanticSearchOptions, error) { if !r.cfg.Search.Embeddings.Enabled { - return store.SemanticSearchOptions{}, fmt.Errorf("embeddings are disabled; enable [search.embeddings] first") + return store.SemanticSearchOptions{}, errors.New("embeddings are disabled; enable [search.embeddings] first") } providerFactory := r.newEmbed if providerFactory == nil { @@ -158,7 +158,7 @@ func (r *runtime) runSQL(args []string) error { return usageErr(err) } if *confirm && !*unsafe { - return usageErr(fmt.Errorf("--confirm requires --unsafe")) + return usageErr(errors.New("--confirm requires --unsafe")) } var query string @@ -184,7 +184,7 @@ func (r *runtime) runSQL(args []string) error { return printRows(r.stdout, cols, rows) } if !*confirm { - return usageErr(fmt.Errorf("--unsafe requires --confirm")) + return usageErr(errors.New("--unsafe requires --confirm")) } if store.IsReadOnlySQL(query) { @@ -207,7 +207,7 @@ func (r *runtime) runSQL(args []string) error { func (r *runtime) runMembers(args []string) error { if len(args) == 0 { - return usageErr(fmt.Errorf("members requires a subcommand")) + return usageErr(errors.New("members requires a subcommand")) } switch args[0] { case "list": @@ -220,7 +220,7 @@ func (r *runtime) runMembers(args []string) error { return r.runMembersShow(args[1:]) case "search": if len(args) < 2 { - return usageErr(fmt.Errorf("members search requires a query")) + return usageErr(errors.New("members search requires a query")) } rows, err := r.store.Members(r.ctx, "", strings.Join(args[1:], " "), 100) if err != nil { @@ -240,7 +240,7 @@ func (r *runtime) runMembersShow(args []string) error { return usageErr(err) } if fs.NArg() < 1 { - return usageErr(fmt.Errorf("members show requires a user id or query")) + return usageErr(errors.New("members show requires a user id or query")) } query := strings.Join(fs.Args(), " ") @@ -282,7 +282,7 @@ func (r *runtime) runMembersShow(args []string) error { func (r *runtime) runChannels(args []string) error { if len(args) == 0 { - return usageErr(fmt.Errorf("channels requires a subcommand")) + return usageErr(errors.New("channels requires a subcommand")) } rows, err := r.store.Channels(r.ctx, "") if err != nil { @@ -293,7 +293,7 @@ func (r *runtime) runChannels(args []string) error { return r.print(rows) case "show": if len(args) < 2 { - return usageErr(fmt.Errorf("channels show requires a channel id")) + return usageErr(errors.New("channels show requires a channel id")) } filtered := make([]store.ChannelRow, 0, 1) for _, row := range rows { diff --git a/internal/cli/query_sync.go b/internal/cli/query_sync.go index c528383..39ecd7b 100644 --- a/internal/cli/query_sync.go +++ b/internal/cli/query_sync.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "fmt" "slices" "strings" @@ -10,7 +11,7 @@ import ( func (r *runtime) syncMessagesQuery(channel, guild, guilds string) error { if r.syncer == nil { - return usageErr(fmt.Errorf("messages --sync requires Discord access")) + return usageErr(errors.New("messages --sync requires Discord access")) } opts, err := r.messageSyncOptions(channel, guild, guilds) if err != nil { @@ -30,7 +31,7 @@ func (r *runtime) messageSyncOptions(channel, guild, guilds string) (syncer.Sync channelFilter := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(channel), "#")) if channelFilter == "" { if len(opts.GuildIDs) == 0 { - return opts, fmt.Errorf("messages --sync needs --channel or --guild") + return opts, errors.New("messages --sync needs --channel or --guild") } return opts, nil } diff --git a/internal/cli/report_commands.go b/internal/cli/report_commands.go index fdb8d45..9e395b2 100644 --- a/internal/cli/report_commands.go +++ b/internal/cli/report_commands.go @@ -1,8 +1,8 @@ package cli import ( + "errors" "flag" - "fmt" "io" "github.com/steipete/discrawl/internal/report" @@ -16,7 +16,7 @@ func (r *runtime) runReport(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("report takes no positional arguments")) + return usageErr(errors.New("report takes no positional arguments")) } activity, err := report.Build(r.ctx, r.store, report.Options{}) if err != nil { diff --git a/internal/cli/share_commands.go b/internal/cli/share_commands.go index cbe2037..476baaa 100644 --- a/internal/cli/share_commands.go +++ b/internal/cli/share_commands.go @@ -1,8 +1,8 @@ package cli import ( + "errors" "flag" - "fmt" "io" "os" @@ -29,7 +29,7 @@ func (r *runtime) runPublish(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("publish takes no positional arguments")) + return usageErr(errors.New("publish takes no positional arguments")) } opts, err := shareOptionsFromFlags(*repoPath, *remote, *branch) if err != nil { @@ -100,7 +100,7 @@ func (r *runtime) runSubscribe(args []string) error { } remote := defaultShareRemote if fs.NArg() > 1 { - return usageErr(fmt.Errorf("subscribe takes at most one remote")) + return usageErr(errors.New("subscribe takes at most one remote")) } if fs.NArg() == 1 { remote = fs.Arg(0) @@ -172,7 +172,7 @@ func (r *runtime) runUpdate(args []string) error { return usageErr(err) } if fs.NArg() != 0 { - return usageErr(fmt.Errorf("update takes no positional arguments")) + return usageErr(errors.New("update takes no positional arguments")) } opts, err := shareOptionsFromFlags(*repoPath, *remote, *branch) if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 40b1385..74f9e35 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -131,7 +131,7 @@ func Default() Config { Concurrency: defaultSyncConcurrency(), RepairEvery: "6h", FullHistory: true, - AttachmentText: boolPtr(true), + AttachmentText: new(true), }, Search: SearchConfig{ DefaultMode: "fts", @@ -262,7 +262,7 @@ func (c *Config) Normalize() error { c.Sync.RepairEvery = "6h" } if c.Sync.AttachmentText == nil { - c.Sync.AttachmentText = boolPtr(true) + c.Sync.AttachmentText = new(true) } if c.Search.DefaultMode == "" { c.Search.DefaultMode = "fts" @@ -497,10 +497,6 @@ func normalizeAccount(account string) string { return account } -func boolPtr(value bool) *bool { - return &value -} - func mapKeys[V any](m map[string]V) []string { keys := make([]string, 0, len(m)) for key := range m { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a031cb4..df0ae0a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -249,7 +249,7 @@ func TestAttachmentTextExplicitFalseSurvivesNormalize(t *testing.T) { t.Parallel() cfg := Default() - cfg.Sync.AttachmentText = boolPtr(false) + cfg.Sync.AttachmentText = new(false) require.NoError(t, cfg.Normalize()) require.False(t, cfg.AttachmentTextEnabled()) } diff --git a/internal/discord/client.go b/internal/discord/client.go index 2f1616f..d35914a 100644 --- a/internal/discord/client.go +++ b/internal/discord/client.go @@ -2,6 +2,7 @@ package discord import ( "context" + "errors" "fmt" "runtime" "slices" @@ -179,7 +180,7 @@ func (c *Client) ChannelMessage(ctx context.Context, channelID, messageID string func (c *Client) Tail(ctx context.Context, handler EventHandler) error { if handler == nil { - return fmt.Errorf("missing event handler") + return errors.New("missing event handler") } tailCtx, cancel := context.WithCancel(ctx) defer cancel() @@ -187,10 +188,8 @@ func (c *Client) Tail(ctx context.Context, handler EventHandler) error { errCh := make(chan error, 1) workCh := make(chan func(context.Context) error, c.tailQueueSize) var wg sync.WaitGroup - for i := 0; i < c.tailWorkerCount; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range c.tailWorkerCount { + wg.Go(func() { for { select { case <-tailCtx.Done(): @@ -207,7 +206,7 @@ func (c *Client) Tail(ctx context.Context, handler EventHandler) error { } } } - }() + }) } c.session.AddHandler(func(_ *discordgo.Session, evt *discordgo.MessageCreate) { @@ -299,7 +298,7 @@ func (c *Client) enqueueTailTask( case workCh <- task: default: select { - case errCh <- fmt.Errorf("tail worker queue full"): + case errCh <- errors.New("tail worker queue full"): default: } } diff --git a/internal/discord/client_test.go b/internal/discord/client_test.go index 87e43f6..32f181e 100644 --- a/internal/discord/client_test.go +++ b/internal/discord/client_test.go @@ -400,7 +400,7 @@ func TestTailFailsFastWhenWorkerQueueFills(t *testing.T) { return } now := time.Now().UTC().Format(time.RFC3339) - for i := 0; i < 4; i++ { + for i := range 4 { if err := conn.WriteJSON(map[string]any{ "op": 0, "t": "MESSAGE_CREATE", diff --git a/internal/discorddesktop/import.go b/internal/discorddesktop/import.go index 606ecd4..3c91dad 100644 --- a/internal/discorddesktop/import.go +++ b/internal/discorddesktop/import.go @@ -467,7 +467,7 @@ func withRawGuildID(rawJSON, guildID string) string { func extractGzipPayloads(data []byte, maxBytes int64) [][]byte { var out [][]byte - for offset := 0; offset < len(data)-1; offset++ { + for offset := range len(data) - 1 { if data[offset] != 0x1f || data[offset+1] != 0x8b { continue } @@ -688,24 +688,24 @@ func messageReferenceID(raw map[string]any) string { } func syntheticGuild(id, name string) store.GuildRecord { - raw, _ := json.Marshal(map[string]any{ + raw := marshalJSONString(map[string]any{ "id": id, "name": name, "source": "discord_desktop", - }) - return store.GuildRecord{ID: id, Name: name, RawJSON: string(raw)} + }, "{}") + return store.GuildRecord{ID: id, Name: name, RawJSON: raw} } func syntheticChannel(id, guildID, name string) store.ChannelRecord { if name == "" { name = "channel-" + shortID(id) } - raw, _ := json.Marshal(map[string]any{ + raw := marshalJSONString(map[string]any{ "id": id, "guild_id": guildID, "name": name, "source": "discord_desktop", - }) + }, "{}") kind := "text" if guildID == DirectMessageGuildID { kind = "dm" @@ -713,7 +713,7 @@ func syntheticChannel(id, guildID, name string) store.ChannelRecord { kind = "group_dm" } } - return store.ChannelRecord{ID: id, GuildID: guildID, Kind: kind, Name: name, RawJSON: string(raw)} + return store.ChannelRecord{ID: id, GuildID: guildID, Kind: kind, Name: name, RawJSON: raw} } func guildName(id string) string { @@ -751,15 +751,14 @@ func kindForChannelType(typeValue int, dm bool) string { } func channelRawJSON(raw map[string]any, id, guildID, name, kind string) string { - body, _ := json.Marshal(map[string]any{ + return marshalJSONString(map[string]any{ "id": id, "guild_id": guildID, "name": name, "kind": kind, "source": "discord_desktop", "type": raw["type"], - }) - return string(body) + }, "{}") } func messageRawJSON(raw map[string]any, id, guildID, channelID, authorID string) string { @@ -780,8 +779,7 @@ func messageRawJSON(raw map[string]any, id, guildID, channelID, authorID string) if author := sanitizedRawAuthor(raw, authorID); len(author) > 0 { payload["author"] = author } - body, _ := json.Marshal(payload) - return string(body) + return marshalJSONString(payload, "{}") } func recipientLabel(items []any) string { @@ -929,3 +927,11 @@ func mapValues[M ~map[string]T, T any](m M) []T { } return out } + +func marshalJSONString(value any, fallback string) string { + raw, err := json.Marshal(value) + if err != nil { + return fallback + } + return string(raw) +} diff --git a/internal/share/share.go b/internal/share/share.go index b530e99..503aa25 100644 --- a/internal/share/share.go +++ b/internal/share/share.go @@ -80,7 +80,7 @@ type EmbeddingManifest struct { func EnsureRepo(ctx context.Context, opts Options) error { if strings.TrimSpace(opts.RepoPath) == "" { - return fmt.Errorf("share repo path is empty") + return errors.New("share repo path is empty") } if _, err := os.Stat(filepath.Join(opts.RepoPath, ".git")); err == nil { return nil @@ -477,7 +477,7 @@ func exportEmbeddings(ctx context.Context, db *sql.DB, opts Options) (EmbeddingM inputVersion = store.EmbeddingInputVersion } if provider == "" || model == "" { - return EmbeddingManifest{}, fmt.Errorf("embedding provider and model are required") + return EmbeddingManifest{}, errors.New("embedding provider and model are required") } relDir := filepath.ToSlash(filepath.Join("embeddings", safePathSegment(provider), safePathSegment(model), safePathSegment(inputVersion))) if err := os.RemoveAll(filepath.Join(opts.RepoPath, "embeddings")); err != nil { diff --git a/internal/store/direct_messages.go b/internal/store/direct_messages.go index 98d2f67..15786c5 100644 --- a/internal/store/direct_messages.go +++ b/internal/store/direct_messages.go @@ -18,8 +18,8 @@ type DirectMessageConversationRow struct { Name string `json:"name"` MessageCount int `json:"message_count"` AuthorCount int `json:"author_count"` - FirstMessageAt time.Time `json:"first_message_at,omitempty"` - LastMessageAt time.Time `json:"last_message_at,omitempty"` + FirstMessageAt time.Time `json:"first_message_at,omitzero"` + LastMessageAt time.Time `json:"last_message_at,omitzero"` } func (s *Store) DirectMessageConversations(ctx context.Context, opts DirectMessageConversationOptions) ([]DirectMessageConversationRow, error) { diff --git a/internal/store/member_profile_extract.go b/internal/store/member_profile_extract.go index 708e446..0865a0a 100644 --- a/internal/store/member_profile_extract.go +++ b/internal/store/member_profile_extract.go @@ -3,6 +3,7 @@ package store import ( "encoding/json" "net/url" + "slices" "sort" "strconv" "strings" @@ -147,10 +148,8 @@ func shouldIgnoreProfileValue(key, value string) bool { } func appendUnique(items []string, value string) []string { - for _, item := range items { - if item == value { - return items - } + if slices.Contains(items, value) { + return items } return append(items, value) } diff --git a/internal/store/members_profile.go b/internal/store/members_profile.go index e016d19..7a0c789 100644 --- a/internal/store/members_profile.go +++ b/internal/store/members_profile.go @@ -13,8 +13,8 @@ type MemberProfile struct { Member MemberRow `json:"member"` RawJSON string `json:"raw_json,omitempty"` MessageCount int `json:"message_count"` - FirstMessageAt time.Time `json:"first_message_at,omitempty"` - LastMessageAt time.Time `json:"last_message_at,omitempty"` + FirstMessageAt time.Time `json:"first_message_at,omitzero"` + LastMessageAt time.Time `json:"last_message_at,omitzero"` RecentMessages []MessageRow `json:"recent_messages,omitempty"` } diff --git a/internal/store/query.go b/internal/store/query.go index 340e41b..d8a533a 100644 --- a/internal/store/query.go +++ b/internal/store/query.go @@ -705,10 +705,10 @@ func (s *Store) Status(ctx context.Context, dbPath, defaultGuildID string) (Stat func (s *Store) ReadOnlyQuery(ctx context.Context, query string) ([]string, [][]string, error) { query = strings.TrimSpace(query) if query == "" { - return nil, nil, fmt.Errorf("empty query") + return nil, nil, errors.New("empty query") } if !IsReadOnlySQL(query) { - return nil, nil, fmt.Errorf("only read-only sql is allowed") + return nil, nil, errors.New("only read-only sql is allowed") } db, closeFn, err := s.openReadOnlyDB() if err != nil { @@ -723,7 +723,7 @@ func (s *Store) ReadOnlyQuery(ctx context.Context, query string) ([]string, [][] func (s *Store) Query(ctx context.Context, query string) ([]string, [][]string, error) { query = strings.TrimSpace(query) if query == "" { - return nil, nil, fmt.Errorf("empty query") + return nil, nil, errors.New("empty query") } return queryRows(ctx, s.db, query) } @@ -731,7 +731,7 @@ func (s *Store) Query(ctx context.Context, query string) ([]string, [][]string, func (s *Store) Exec(ctx context.Context, query string) (int64, error) { query = strings.TrimSpace(query) if query == "" { - return 0, fmt.Errorf("empty query") + return 0, errors.New("empty query") } queryCtx, cancel := withQueryTimeout(ctx) defer cancel() @@ -762,7 +762,7 @@ func queryRows(ctx context.Context, db *sql.DB, query string) ([]string, [][]str return nil, nil, err } if len(cols) == 0 { - return nil, nil, fmt.Errorf("query returned no columns") + return nil, nil, errors.New("query returned no columns") } var out [][]string diff --git a/internal/store/store.go b/internal/store/store.go index 62b23f4..7da630d 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -34,8 +34,8 @@ type Status struct { MessageCount int `json:"message_count"` MemberCount int `json:"member_count"` EmbeddingBacklog int `json:"embedding_backlog"` - LastSyncAt time.Time `json:"last_sync_at,omitempty"` - LastTailEventAt time.Time `json:"last_tail_event_at,omitempty"` + LastSyncAt time.Time `json:"last_sync_at,omitzero"` + LastTailEventAt time.Time `json:"last_tail_event_at,omitzero"` DefaultGuildID string `json:"default_guild_id,omitempty"` DefaultGuildName string `json:"default_guild_name,omitempty"` AccessibleGuildIDs []string `json:"accessible_guild_ids,omitempty"` @@ -86,7 +86,7 @@ type MemberRow struct { Avatar string `json:"avatar,omitempty"` RoleIDsJSON string `json:"role_ids_json"` Bot bool `json:"bot"` - JoinedAt time.Time `json:"joined_at,omitempty"` + JoinedAt time.Time `json:"joined_at,omitzero"` Bio string `json:"bio,omitempty"` Pronouns string `json:"pronouns,omitempty"` Location string `json:"location,omitempty"` @@ -110,7 +110,7 @@ type ChannelRow struct { IsLocked bool `json:"is_locked"` IsPrivateThread bool `json:"is_private_thread"` ThreadParentID string `json:"thread_parent_id,omitempty"` - ArchiveTimestamp time.Time `json:"archive_timestamp,omitempty"` + ArchiveTimestamp time.Time `json:"archive_timestamp,omitzero"` } func Open(ctx context.Context, path string) (*Store, error) { diff --git a/internal/store/store_test.go b/internal/store/store_test.go index f4b2a10..5f185c6 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -603,7 +603,7 @@ func TestSearchMessagesSemanticScoresOlderMatchesBeyondRecentWindow(t *testing.T })) require.NoError(t, insertTestEmbedding(ctx, s, "old-best", "ollama", "nomic-embed-text", []float32{1, 0})) - for i := 0; i < searchCandidateFloor+10; i++ { + for i := range searchCandidateFloor + 10 { messageID := "newer-weak-" + strconv.Itoa(i) require.NoError(t, s.UpsertMessage(ctx, MessageRecord{ ID: messageID, diff --git a/internal/store/store_write_test.go b/internal/store/store_write_test.go index 7cb4db9..fa31f1e 100644 --- a/internal/store/store_write_test.go +++ b/internal/store/store_write_test.go @@ -586,7 +586,7 @@ func TestConcurrentMessageUpsertsShareSingleWriter(t *testing.T) { var wg sync.WaitGroup errCh := make(chan error, 8) - for i := 0; i < 8; i++ { + for i := range 8 { wg.Add(1) go func(i int) { defer wg.Done() diff --git a/internal/syncer/channel_catalog_test.go b/internal/syncer/channel_catalog_test.go index cd8a2a9..3b7d089 100644 --- a/internal/syncer/channel_catalog_test.go +++ b/internal/syncer/channel_catalog_test.go @@ -2,7 +2,7 @@ package syncer import ( "context" - "fmt" + "errors" "path/filepath" "testing" "time" @@ -14,7 +14,7 @@ import ( ) func errMissingAccess() error { - return fmt.Errorf("HTTP 403 Forbidden, {\"message\": \"Missing Access\", \"code\": 50001}") + return errors.New("HTTP 403 Forbidden, {\"message\": \"Missing Access\", \"code\": 50001}") } func TestActiveThreadCatalogEdges(t *testing.T) { @@ -34,7 +34,7 @@ func TestActiveThreadCatalogEdges(t *testing.T) { require.NoError(t, svc.appendActiveThreadCatalog(ctx, allChannels, "g1", []string{"forum"})) require.Equal(t, 1, client.guildThreadCalls) - client.guildThreadErrs = map[string]error{"g1": fmt.Errorf("boom")} + client.guildThreadErrs = map[string]error{"g1": errors.New("boom")} require.ErrorContains(t, svc.appendActiveThreadCatalog(ctx, allChannels, "g1", []string{"forum"}), "boom") client.guildThreadErrs = nil diff --git a/internal/syncer/message_sync.go b/internal/syncer/message_sync.go index d7726d1..9dccd50 100644 --- a/internal/syncer/message_sync.go +++ b/internal/syncer/message_sync.go @@ -121,9 +121,7 @@ func (s *Syncer) syncMessageChannelsConcurrent( var wg sync.WaitGroup for range workers { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for channel := range jobs { if ctx.Err() != nil { return @@ -151,7 +149,7 @@ func (s *Syncer) syncMessageChannelsConcurrent( return } } - }() + }) } go func() { diff --git a/internal/syncer/message_sync_progress_test.go b/internal/syncer/message_sync_progress_test.go index 2dd5832..e266a9b 100644 --- a/internal/syncer/message_sync_progress_test.go +++ b/internal/syncer/message_sync_progress_test.go @@ -3,7 +3,7 @@ package syncer import ( "bytes" "context" - "fmt" + "errors" "io" "log/slog" "strings" @@ -63,9 +63,9 @@ func TestMessageSyncProgressFinishReportsSummaryCounts(t *testing.T) { third := &discordgo.Channel{ID: "c3", Name: "ok"} progress.start(first) - progress.recordSkip(first, fmt.Errorf(`HTTP 403 Forbidden, {"message": "Missing Access", "code": 50001}`)) + progress.recordSkip(first, errors.New(`HTTP 403 Forbidden, {"message": "Missing Access", "code": 50001}`)) progress.start(second) - progress.recordSkip(second, fmt.Errorf(`HTTP 404 Not Found, {"message": "Unknown Channel", "code": 10003}`)) + progress.recordSkip(second, errors.New(`HTTP 404 Not Found, {"message": "Unknown Channel", "code": 10003}`)) progress.start(third) progress.record(third, 42) progress.finish(nil) diff --git a/internal/syncer/records.go b/internal/syncer/records.go index 0eff966..f15c28f 100644 --- a/internal/syncer/records.go +++ b/internal/syncer/records.go @@ -13,8 +13,8 @@ import ( ) func toMemberRecord(guildID string, member *discordgo.Member) store.MemberRecord { - raw, _ := json.Marshal(member) - roles, _ := json.Marshal(member.Roles) + raw := marshalJSONString(member, "{}") + roles := marshalJSONString(member.Roles, "[]") return store.MemberRecord{ GuildID: guildID, UserID: member.User.ID, @@ -26,13 +26,13 @@ func toMemberRecord(guildID string, member *discordgo.Member) store.MemberRecord Avatar: member.Avatar, Bot: member.User.Bot, JoinedAt: member.JoinedAt.Format(time.RFC3339Nano), - RoleIDsJSON: string(roles), - RawJSON: string(raw), + RoleIDsJSON: roles, + RawJSON: raw, } } func toMessageRecord(message *discordgo.Message, channelName, normalizedContent string) store.MessageRecord { - raw, _ := json.Marshal(message) + raw := marshalJSONString(message, "{}") authorID := "" authorName := "" if message.Author != nil { @@ -65,10 +65,18 @@ func toMessageRecord(message *discordgo.Message, channelName, normalizedContent ReplyToMessageID: replyTo, Pinned: message.Pinned, HasAttachments: len(message.Attachments) > 0, - RawJSON: string(raw), + RawJSON: raw, } } +func marshalJSONString(value any, fallback string) string { + raw, err := json.Marshal(value) + if err != nil { + return fallback + } + return string(raw) +} + func normalizeMessage(message *discordgo.Message) string { return normalizeMessageParts(message, nil) } diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go index 4beac96..b417ffc 100644 --- a/internal/syncer/syncer.go +++ b/internal/syncer/syncer.go @@ -2,7 +2,6 @@ package syncer import ( "context" - "encoding/json" "errors" "fmt" "log/slog" @@ -124,12 +123,11 @@ func (s *Syncer) syncGuild(ctx context.Context, guildID string, opts SyncOptions if err != nil { return SyncStats{}, fmt.Errorf("fetch guild %s: %w", guildID, err) } - rawGuild, _ := json.Marshal(guild) if err := s.store.UpsertGuild(ctx, store.GuildRecord{ ID: guild.ID, Name: guild.Name, Icon: guild.Icon, - RawJSON: string(rawGuild), + RawJSON: marshalJSONString(guild, "{}"), }); err != nil { return SyncStats{}, err } @@ -160,8 +158,7 @@ func (s *Syncer) syncGuild(ctx context.Context, guildID string, opts SyncOptions return stats, err } for _, channel := range channelList { - raw, _ := json.Marshal(channel) - record := toChannelRecord(channel, string(raw)) + record := toChannelRecord(channel, marshalJSONString(channel, "{}")) if err := s.store.UpsertChannel(ctx, record); err != nil { return stats, err } @@ -204,10 +201,7 @@ func (s *Syncer) syncGuildIncompleteBatches(ctx context.Context, guildID string, } stats := SyncStats{} for start := 0; start < len(incomplete); start += fullSyncBatchSize { - end := start + fullSyncBatchSize - if end > len(incomplete) { - end = len(incomplete) - } + end := min(start+fullSyncBatchSize, len(incomplete)) batchOpts := opts batchOpts.ChannelIDs = incomplete[start:end] one, err := s.syncGuild(ctx, guildID, batchOpts) diff --git a/internal/syncer/syncer_tail_test.go b/internal/syncer/syncer_tail_test.go index eacdafb..004ca1d 100644 --- a/internal/syncer/syncer_tail_test.go +++ b/internal/syncer/syncer_tail_test.go @@ -3,7 +3,6 @@ package syncer import ( "context" "errors" - "fmt" "path/filepath" "testing" "time" @@ -186,16 +185,16 @@ func TestHelpers(t *testing.T) { require.Equal(t, "Nick", displayName(&discordgo.Member{Nick: "Nick", User: &discordgo.User{Username: "user"}})) require.Equal(t, "Global", displayName(&discordgo.Member{User: &discordgo.User{GlobalName: "Global", Username: "user"}})) require.Equal(t, "user", displayName(&discordgo.Member{User: &discordgo.User{Username: "user"}})) - require.True(t, isMissingAccess(fmt.Errorf("HTTP 403 Forbidden"))) - require.True(t, isMissingAccess(fmt.Errorf("Missing Access"))) - require.False(t, isMissingAccess(fmt.Errorf("boom"))) - require.Equal(t, "missing_access", unavailableReason(fmt.Errorf("HTTP 403 Forbidden"))) - require.Equal(t, "unknown_channel", unavailableReason(fmt.Errorf("HTTP 404 Not Found, {\"message\": \"Unknown Channel\", \"code\": 10003}"))) - require.True(t, isUnknownChannel(fmt.Errorf("Unknown Channel"))) - require.False(t, isUnknownChannel(fmt.Errorf("boom"))) + require.True(t, isMissingAccess(errors.New("HTTP 403 Forbidden"))) + require.True(t, isMissingAccess(errors.New("Missing Access"))) + require.False(t, isMissingAccess(errors.New("boom"))) + require.Equal(t, "missing_access", unavailableReason(errors.New("HTTP 403 Forbidden"))) + require.Equal(t, "unknown_channel", unavailableReason(errors.New("HTTP 404 Not Found, {\"message\": \"Unknown Channel\", \"code\": 10003}"))) + require.True(t, isUnknownChannel(errors.New("Unknown Channel"))) + require.False(t, isUnknownChannel(errors.New("boom"))) require.True(t, isRetryableSyncError(context.Background(), context.DeadlineExceeded)) - require.True(t, isRetryableSyncError(context.Background(), fmt.Errorf("HTTP 503 Service Unavailable"))) - require.True(t, isRetryableSyncError(context.Background(), fmt.Errorf("stream error: stream ID 1; INTERNAL_ERROR"))) + require.True(t, isRetryableSyncError(context.Background(), errors.New("HTTP 503 Service Unavailable"))) + require.True(t, isRetryableSyncError(context.Background(), errors.New("stream error: stream ID 1; INTERNAL_ERROR"))) require.False(t, isRetryableSyncError(context.Background(), context.Canceled)) canceledCtx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/internal/syncer/syncer_test.go b/internal/syncer/syncer_test.go index a550dce..7c109dc 100644 --- a/internal/syncer/syncer_test.go +++ b/internal/syncer/syncer_test.go @@ -2,7 +2,7 @@ package syncer import ( "context" - "fmt" + "errors" "path/filepath" "sync" "testing" @@ -777,7 +777,7 @@ func TestSyncSkipsMissingAccessChannels(t *testing.T) { "c1": {{ID: "10", GuildID: "g1", ChannelID: "c1", Content: "ok", Timestamp: time.Now().UTC(), Author: &discordgo.User{ID: "u1", Username: "user"}}}, }, messageErrors: map[string]error{ - "c2": fmt.Errorf("HTTP 403 Forbidden, {\"message\": \"Missing Access\", \"code\": 50001}"), + "c2": errors.New("HTTP 403 Forbidden, {\"message\": \"Missing Access\", \"code\": 50001}"), }, } @@ -814,7 +814,7 @@ func TestSyncSkipsUnknownChannels(t *testing.T) { "c1": {{ID: "10", GuildID: "g1", ChannelID: "c1", Content: "ok", Timestamp: time.Now().UTC(), Author: &discordgo.User{ID: "u1", Username: "user"}}}, }, messageErrors: map[string]error{ - "c2": fmt.Errorf("HTTP 404 Not Found, {\"message\": \"Unknown Channel\", \"code\": 10003}"), + "c2": errors.New("HTTP 404 Not Found, {\"message\": \"Unknown Channel\", \"code\": 10003}"), }, } diff --git a/internal/syncer/tail.go b/internal/syncer/tail.go index 1829de1..10caad5 100644 --- a/internal/syncer/tail.go +++ b/internal/syncer/tail.go @@ -2,7 +2,6 @@ package syncer import ( "context" - "encoding/json" "time" "github.com/bwmarrin/discordgo" @@ -97,8 +96,7 @@ func (t *tailHandler) OnChannelUpsert(ctx context.Context, channel *discordgo.Ch if !t.allowGuild(channel.GuildID) { return nil } - raw, _ := json.Marshal(channel) - return t.store.UpsertChannel(ctx, toChannelRecord(channel, string(raw))) + return t.store.UpsertChannel(ctx, toChannelRecord(channel, marshalJSONString(channel, "{}"))) } func (t *tailHandler) OnMemberUpsert(ctx context.Context, guildID string, member *discordgo.Member) error {