From aa9c0fe041cd73637f5e373a96572b55c30a82ca Mon Sep 17 00:00:00 2001 From: Tyler Hawkins <3319104+tyzbit@users.noreply.github.com> Date: Mon, 6 Feb 2023 15:58:03 -0500 Subject: [PATCH] feat: better error handling, bump go-archive dep, retries --- bot/message_handlers.go | 22 +- bot/servers.go | 75 +++- go.mod | 3 +- go.sum | 10 + vendor/github.com/avast/retry-go/.gitignore | 21 + .../github.com/avast/retry-go/.godocdown.tmpl | 37 ++ vendor/github.com/avast/retry-go/.travis.yml | 20 + vendor/github.com/avast/retry-go/Gopkg.toml | 3 + vendor/github.com/avast/retry-go/LICENSE | 21 + vendor/github.com/avast/retry-go/Makefile | 65 ++++ vendor/github.com/avast/retry-go/README.md | 361 ++++++++++++++++++ vendor/github.com/avast/retry-go/VERSION | 1 + vendor/github.com/avast/retry-go/appveyor.yml | 19 + vendor/github.com/avast/retry-go/options.go | 198 ++++++++++ vendor/github.com/avast/retry-go/retry.go | 225 +++++++++++ vendor/github.com/tyzbit/go-archive/main.go | 58 ++- vendor/modules.txt | 5 +- 17 files changed, 1100 insertions(+), 44 deletions(-) create mode 100644 vendor/github.com/avast/retry-go/.gitignore create mode 100644 vendor/github.com/avast/retry-go/.godocdown.tmpl create mode 100644 vendor/github.com/avast/retry-go/.travis.yml create mode 100644 vendor/github.com/avast/retry-go/Gopkg.toml create mode 100644 vendor/github.com/avast/retry-go/LICENSE create mode 100644 vendor/github.com/avast/retry-go/Makefile create mode 100644 vendor/github.com/avast/retry-go/README.md create mode 100644 vendor/github.com/avast/retry-go/VERSION create mode 100644 vendor/github.com/avast/retry-go/appveyor.yml create mode 100644 vendor/github.com/avast/retry-go/options.go create mode 100644 vendor/github.com/avast/retry-go/retry.go diff --git a/bot/message_handlers.go b/bot/message_handlers.go index 57e121c..14ff31f 100644 --- a/bot/message_handlers.go +++ b/bot/message_handlers.go @@ -108,19 +108,19 @@ func (bot *ArchiverBot) handleArchiveRequest(s *discordgo.Session, r *discordgo. return fmt.Errorf("unable to look up message by id: %v", r.MessageID) } xurlsStrict := xurls.Strict - urls := xurlsStrict.FindAllString(message.Content, -1) - if len(urls) == 0 { + messageUrls := xurlsStrict.FindAllString(message.Content, -1) + if len(messageUrls) == 0 { return fmt.Errorf("found 0 URLs in message") } - log.Debug("URLs parsed from message: ", strings.Join(urls, ", ")) + log.Debug("URLs parsed from message: ", strings.Join(messageUrls, ", ")) // This UUID will be used to tie together the ArchiveEventEvent, // the archiveRequestUrls and the archiveResponseUrls. archiveEventUUID := uuid.New().String() var archives []ArchiveEvent - for _, url := range urls { + for _, url := range messageUrls { domainName, err := getDomainName(url) if err != nil { log.Error("unable to get domain name for url: ", url) @@ -174,7 +174,7 @@ func (bot *ArchiverBot) handleArchiveRequest(s *discordgo.Session, r *discordgo. if archive.ResponseURL == "" { log.Debug("need to call archive.org api for ", archive.RequestURL) - urls = []string{} + urls := []string{} if rearchive { url, err := goarchive.ArchiveURL(archive.RequestURL) if err != nil || url == "" { @@ -184,7 +184,7 @@ func (bot *ArchiverBot) handleArchiveRequest(s *discordgo.Session, r *discordgo. urls = append(urls, url) } else { var errs []error - urls, errs = goarchive.GetLatestURLs([]string{archive.RequestURL}, ServerConfig.AutoArchive) + urls, errs = goarchive.GetLatestURLs([]string{archive.RequestURL}, ServerConfig.RetryAttempts, ServerConfig.AutoArchive) for _, err := range errs { if err != nil { log.Errorf("error archiving url: %v", err) @@ -212,6 +212,16 @@ func (bot *ArchiverBot) handleArchiveRequest(s *discordgo.Session, r *discordgo. } } + if len(archivedLinks) < len(messageUrls) { + log.Errorf("did not receive the same number of archived links as submitted URLs") + if len(archivedLinks) == 0 { + log.Errorf("did not receive any Archive.org links") + archivedLinks = []string{"I was unable to get any Wayback Machine URLs. " + + "Most of the time, this is " + + "due to rate-limiting by Archive.org. " + + "Please try again by adding the 'repeat' emoji to the message."} + } + } plural := "" if len(archivedLinks) > 1 { plural = "s" diff --git a/bot/servers.go b/bot/servers.go index 07a3e63..2e27664 100644 --- a/bot/servers.go +++ b/bot/servers.go @@ -2,6 +2,7 @@ package bot import ( "fmt" + "strconv" "strings" "time" @@ -24,8 +25,14 @@ type ServerConfig struct { ReplyToOriginalMessage bool `pretty:"Reply to original message (embed must be off) (replyto)"` UseEmbed bool `pretty:"Use embed to reply (embed)"` AutoArchive bool `pretty:"Automatically try archiving a page if it is not found (archive)"` + RetryAttempts uint `pretty:"Number of attempts the bot should make to archive a URL. Max 10"` } +const ( + MinAllowedRetryAttempts = 1 + MaxAllowedRetryAttempts = 10 +) + var ( defaultServerConfig ServerConfig = ServerConfig{ DiscordId: "0", @@ -34,6 +41,7 @@ var ( ReplyToOriginalMessage: false, UseEmbed: true, AutoArchive: true, + RetryAttempts: 4, } archiverRepoUrl string = "https://github.com/tyzbit/go-discord-archiver" @@ -90,6 +98,7 @@ func (bot *ArchiverBot) getServerConfig(guildId string) ServerConfig { // setServerConfig sets a single config setting for the calling server. Syntax: // (commandPrefix) config [setting] [value] func (bot *ArchiverBot) setServerConfig(s *discordgo.Session, m *discordgo.Message) error { + var scError error // Look up the guild from the message guild, err := s.Guild(m.GuildID) if err != nil { @@ -107,9 +116,9 @@ func (bot *ArchiverBot) setServerConfig(s *discordgo.Session, m *discordgo.Messa command := strings.Split(m.Content, " ") var setting, value string - if len(command) == 4 { - setting = command[2] - value = command[3] + if len(command) == 5 { + setting = command[3] + value = command[4] } else { setting = "get" } @@ -120,6 +129,16 @@ func (bot *ArchiverBot) setServerConfig(s *discordgo.Session, m *discordgo.Messa } tx := &gorm.DB{} + var valueIsStringBoolean = false + var valueIsNumber = false + var description string + var errDetails string + if (value == "on" || value == "off") { + valueIsStringBoolean = true + } + if _, err := strconv.Atoi(value); err == nil { + valueIsNumber = true + } switch setting { // "get" is the only command that does not alter the database. case "get": @@ -129,13 +148,34 @@ func (bot *ArchiverBot) setServerConfig(s *discordgo.Session, m *discordgo.Messa }) return nil case "switch": - tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("archive_enabled", value == "on") + if valueIsStringBoolean { + tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("archive_enabled", value == "on") + } case "replyto": - tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("reply_to_original_message", value == "on") + if valueIsStringBoolean { + tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("reply_to_original_message", value == "on") + } case "embed": - tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("use_embed", value == "on") + if valueIsStringBoolean { + tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("use_embed", value == "on") + } case "archive": - tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("auto_archive", value == "on") + if valueIsStringBoolean { + tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("auto_archive", value == "on") + } + case "attempts": + if valueIsNumber { + uintValue, err := strconv.ParseUint(value, 10, 0) + if err != nil { + errDetails = "not a number." + } + if (uintValue >= MinAllowedRetryAttempts && uintValue <= MaxAllowedRetryAttempts) { + tx = bot.DB.Model(&ServerConfig{}).Where(&ServerConfig{DiscordId: guild.ID}).Update("retry_attempts", uintValue) + } else { + errDetails = "not between " + + fmt.Sprint(MinAllowedRetryAttempts) + " and " + fmt.Sprint(MaxAllowedRetryAttempts) + "." + } + } default: bot.sendMessage(s, sc.UseEmbed, sc.ReplyToOriginalMessage, m, errorEmbed) return nil @@ -143,16 +183,19 @@ func (bot *ArchiverBot) setServerConfig(s *discordgo.Session, m *discordgo.Messa // We only expect one server to be updated at a time. Otherwise, return an error. if tx.RowsAffected != 1 { - return fmt.Errorf("did not expect %v rows to be affected updating "+ - "server config for server: %v(%v)", fmt.Sprintf("%v", tx.RowsAffected), guild.Name, guild.ID) + errorEmbed.Title = "Unable to set " + setting + " to " + value + ", " + errDetails + bot.sendMessage(s, sc.UseEmbed, sc.ReplyToOriginalMessage, m, errorEmbed) + scError = fmt.Errorf("did not expect %v rows to be affected updating "+ + "server config for server: %v(%v)", fmt.Sprintf("%v", tx.RowsAffected), guild.Name, guild.ID) + } else { + description = setting + " set to " + value + scError = nil + bot.sendMessage(s, sc.UseEmbed, sc.ReplyToOriginalMessage, m, &discordgo.MessageEmbed{ + Title: "Setting Updated", + Description: description, + }) } - - bot.sendMessage(s, sc.UseEmbed, sc.ReplyToOriginalMessage, m, &discordgo.MessageEmbed{ - Title: "Setting Updated", - Description: setting + " set to " + value, - }) - - return nil + return scError } // updateServersWatched updates the servers watched value diff --git a/go.mod b/go.mod index df74ccb..0e6dd5f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/google/uuid v1.3.0 github.com/mvdan/xurls v1.1.0 github.com/sirupsen/logrus v1.8.1 - github.com/tyzbit/go-archive v0.0.0-20220422014241-aa077f0a8174 gorm.io/driver/mysql v1.3.3 gorm.io/driver/sqlite v1.3.1 gorm.io/gorm v1.23.4 @@ -16,6 +15,7 @@ require ( require ( github.com/BurntSushi/toml v0.4.1 // indirect + github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-gonic/gin v1.7.7 // indirect github.com/go-playground/locales v0.13.0 // indirect @@ -35,6 +35,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.9 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/tyzbit/go-archive v0.0.0-20230206195843-20514a4efc44 // indirect github.com/ugorji/go/codec v1.1.7 // indirect golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect diff --git a/go.sum b/go.sum index 0a3afd1..eb6c673 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs= github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -67,6 +69,14 @@ github.com/tyzbit/go-archive v0.0.0-20220422011230-e4ccafb403ab h1:98S4kV6VGEV9F github.com/tyzbit/go-archive v0.0.0-20220422011230-e4ccafb403ab/go.mod h1:+lKbHcRfCD67HK5VtyeMKQAsdZlbP5eq08sTlWzPQq4= github.com/tyzbit/go-archive v0.0.0-20220422014241-aa077f0a8174 h1:0L+g6CllW/VPERnQcWc4FtTeGBiNkIIdK3HBupbn2aI= github.com/tyzbit/go-archive v0.0.0-20220422014241-aa077f0a8174/go.mod h1:+lKbHcRfCD67HK5VtyeMKQAsdZlbP5eq08sTlWzPQq4= +github.com/tyzbit/go-archive v0.0.0-20230206185234-e0ab8e1dc3de h1:v/UKYFZspLSbhTDoTqkUQAetYWsAemQyxh0R+2b6hzA= +github.com/tyzbit/go-archive v0.0.0-20230206185234-e0ab8e1dc3de/go.mod h1:D8vNUS0ZU4ILov2Igxw1h+Kj7aeyapjUU1ruPLEx9gw= +github.com/tyzbit/go-archive v0.0.0-20230206192309-94b220444e20 h1:bsOewcV4u7FFxaunSAqW9uwU2iaqOR+5DsisO0NRqzM= +github.com/tyzbit/go-archive v0.0.0-20230206192309-94b220444e20/go.mod h1:D8vNUS0ZU4ILov2Igxw1h+Kj7aeyapjUU1ruPLEx9gw= +github.com/tyzbit/go-archive v0.0.0-20230206193458-f613421690a2 h1:adhdKsU62u6V50w5Wqw7RYR9DUhUpPS4rxuqdQfgN0g= +github.com/tyzbit/go-archive v0.0.0-20230206193458-f613421690a2/go.mod h1:D8vNUS0ZU4ILov2Igxw1h+Kj7aeyapjUU1ruPLEx9gw= +github.com/tyzbit/go-archive v0.0.0-20230206195843-20514a4efc44 h1:eFnPPxL7hFNDJ+EKmW6OTk8MEr5C04+2gKqsP1C10F8= +github.com/tyzbit/go-archive v0.0.0-20230206195843-20514a4efc44/go.mod h1:D8vNUS0ZU4ILov2Igxw1h+Kj7aeyapjUU1ruPLEx9gw= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= diff --git a/vendor/github.com/avast/retry-go/.gitignore b/vendor/github.com/avast/retry-go/.gitignore new file mode 100644 index 0000000..c40eb23 --- /dev/null +++ b/vendor/github.com/avast/retry-go/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# dep +vendor/ +Gopkg.lock + +# cover +coverage.txt diff --git a/vendor/github.com/avast/retry-go/.godocdown.tmpl b/vendor/github.com/avast/retry-go/.godocdown.tmpl new file mode 100644 index 0000000..6873edf --- /dev/null +++ b/vendor/github.com/avast/retry-go/.godocdown.tmpl @@ -0,0 +1,37 @@ +# {{ .Name }} + +[![Release](https://img.shields.io/github/release/avast/retry-go.svg?style=flat-square)](https://github.com/avast/retry-go/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Travis](https://img.shields.io/travis/avast/retry-go.svg?style=flat-square)](https://travis-ci.org/avast/retry-go) +[![AppVeyor](https://ci.appveyor.com/api/projects/status/fieg9gon3qlq0a9a?svg=true)](https://ci.appveyor.com/project/JaSei/retry-go) +[![Go Report Card](https://goreportcard.com/badge/github.com/avast/retry-go?style=flat-square)](https://goreportcard.com/report/github.com/avast/retry-go) +[![GoDoc](https://godoc.org/github.com/avast/retry-go?status.svg&style=flat-square)](http://godoc.org/github.com/avast/retry-go) +[![codecov.io](https://codecov.io/github/avast/retry-go/coverage.svg?branch=master)](https://codecov.io/github/avast/retry-go?branch=master) +[![Sourcegraph](https://sourcegraph.com/github.com/avast/retry-go/-/badge.svg)](https://sourcegraph.com/github.com/avast/retry-go?badge) + +{{ .EmitSynopsis }} + +{{ .EmitUsage }} + +## Contributing + +Contributions are very much welcome. + +### Makefile + +Makefile provides several handy rules, like README.md `generator` , `setup` for prepare build/dev environment, `test`, `cover`, etc... + +Try `make help` for more information. + +### Before pull request + +please try: +* run tests (`make test`) +* run linter (`make lint`) +* if your IDE don't automaticaly do `go fmt`, run `go fmt` (`make fmt`) + +### README + +README.md are generate from template [.godocdown.tmpl](.godocdown.tmpl) and code documentation via [godocdown](https://github.com/robertkrimen/godocdown). + +Never edit README.md direct, because your change will be lost. diff --git a/vendor/github.com/avast/retry-go/.travis.yml b/vendor/github.com/avast/retry-go/.travis.yml new file mode 100644 index 0000000..ae3e0b6 --- /dev/null +++ b/vendor/github.com/avast/retry-go/.travis.yml @@ -0,0 +1,20 @@ +language: go + +go: + - 1.8 + - 1.9 + - "1.10" + - 1.11 + - 1.12 + - 1.13 + - 1.14 + - 1.15 + +install: + - make setup + +script: + - make ci + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/avast/retry-go/Gopkg.toml b/vendor/github.com/avast/retry-go/Gopkg.toml new file mode 100644 index 0000000..cf8c9eb --- /dev/null +++ b/vendor/github.com/avast/retry-go/Gopkg.toml @@ -0,0 +1,3 @@ +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.1.4" diff --git a/vendor/github.com/avast/retry-go/LICENSE b/vendor/github.com/avast/retry-go/LICENSE new file mode 100644 index 0000000..f63fca8 --- /dev/null +++ b/vendor/github.com/avast/retry-go/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Avast + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/avast/retry-go/Makefile b/vendor/github.com/avast/retry-go/Makefile new file mode 100644 index 0000000..769816d --- /dev/null +++ b/vendor/github.com/avast/retry-go/Makefile @@ -0,0 +1,65 @@ +SOURCE_FILES?=$$(go list ./... | grep -v /vendor/) +TEST_PATTERN?=. +TEST_OPTIONS?= +DEP?=$$(which dep) +VERSION?=$$(cat VERSION) +LINTER?=$$(which golangci-lint) +LINTER_VERSION=1.15.0 + +ifeq ($(OS),Windows_NT) + DEP_VERS=dep-windows-amd64 + LINTER_FILE=golangci-lint-$(LINTER_VERSION)-windows-amd64.zip + LINTER_UNPACK= >| app.zip; unzip -j app.zip -d $$GOPATH/bin; rm app.zip +else ifeq ($(OS), Darwin) + LINTER_FILE=golangci-lint-$(LINTER_VERSION)-darwin-amd64.tar.gz + LINTER_UNPACK= | tar xzf - -C $$GOPATH/bin --wildcards --strip 1 "**/golangci-lint" +else + DEP_VERS=dep-linux-amd64 + LINTER_FILE=golangci-lint-$(LINTER_VERSION)-linux-amd64.tar.gz + LINTER_UNPACK= | tar xzf - -C $$GOPATH/bin --wildcards --strip 1 "**/golangci-lint" +endif + +setup: + go get -u github.com/pierrre/gotestcover + go get -u golang.org/x/tools/cmd/cover + go get -u github.com/robertkrimen/godocdown/godocdown + @if [ "$(LINTER)" = "" ]; then\ + curl -L https://github.com/golangci/golangci-lint/releases/download/v$(LINTER_VERSION)/$(LINTER_FILE) $(LINTER_UNPACK) ;\ + chmod +x $$GOPATH/bin/golangci-lint;\ + fi + @if [ "$(DEP)" = "" ]; then\ + curl -L https://github.com/golang/dep/releases/download/v0.3.1/$(DEP_VERS) >| $$GOPATH/bin/dep;\ + chmod +x $$GOPATH/bin/dep;\ + fi + dep ensure + +generate: ## Generate README.md + godocdown >| README.md + +test: generate test_and_cover_report lint + +test_and_cover_report: + gotestcover $(TEST_OPTIONS) -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m + +cover: test ## Run all the tests and opens the coverage report + go tool cover -html=coverage.txt + +fmt: ## gofmt and goimports all go files + find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done + +lint: ## Run all the linters + golangci-lint run + +ci: test_and_cover_report ## Run all the tests but no linters - use https://golangci.com integration instead + +build: + go build + +release: ## Release new version + git tag | grep -q $(VERSION) && echo This version was released! Increase VERSION! || git tag $(VERSION) && git push origin $(VERSION) && git tag v$(VERSION) && git push origin v$(VERSION) + +# Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := build diff --git a/vendor/github.com/avast/retry-go/README.md b/vendor/github.com/avast/retry-go/README.md new file mode 100644 index 0000000..80fb73b --- /dev/null +++ b/vendor/github.com/avast/retry-go/README.md @@ -0,0 +1,361 @@ +# retry + +[![Release](https://img.shields.io/github/release/avast/retry-go.svg?style=flat-square)](https://github.com/avast/retry-go/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Travis](https://img.shields.io/travis/avast/retry-go.svg?style=flat-square)](https://travis-ci.org/avast/retry-go) +[![AppVeyor](https://ci.appveyor.com/api/projects/status/fieg9gon3qlq0a9a?svg=true)](https://ci.appveyor.com/project/JaSei/retry-go) +[![Go Report Card](https://goreportcard.com/badge/github.com/avast/retry-go?style=flat-square)](https://goreportcard.com/report/github.com/avast/retry-go) +[![GoDoc](https://godoc.org/github.com/avast/retry-go?status.svg&style=flat-square)](http://godoc.org/github.com/avast/retry-go) +[![codecov.io](https://codecov.io/github/avast/retry-go/coverage.svg?branch=master)](https://codecov.io/github/avast/retry-go?branch=master) +[![Sourcegraph](https://sourcegraph.com/github.com/avast/retry-go/-/badge.svg)](https://sourcegraph.com/github.com/avast/retry-go?badge) + +Simple library for retry mechanism + +slightly inspired by +[Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry) + + +### SYNOPSIS + +http get with retry: + + url := "http://example.com" + var body []byte + + err := retry.Do( + func() error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return nil + }, + ) + + fmt.Println(body) + +[next examples](https://github.com/avast/retry-go/tree/master/examples) + + +### SEE ALSO + +* [giantswarm/retry-go](https://github.com/giantswarm/retry-go) - slightly +complicated interface. + +* [sethgrid/pester](https://github.com/sethgrid/pester) - only http retry for +http calls with retries and backoff + +* [cenkalti/backoff](https://github.com/cenkalti/backoff) - Go port of the +exponential backoff algorithm from Google's HTTP Client Library for Java. Really +complicated interface. + +* [rafaeljesus/retry-go](https://github.com/rafaeljesus/retry-go) - looks good, +slightly similar as this package, don't have 'simple' `Retry` method + +* [matryer/try](https://github.com/matryer/try) - very popular package, +nonintuitive interface (for me) + + +### BREAKING CHANGES + +3.0.0 + +* `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects +only your custom Delay Functions. This change allow [make delay functions based +on error](examples/delay_based_on_error_test.go). + +1.0.2 -> 2.0.0 + +* argument of `retry.Delay` is final delay (no multiplication by `retry.Units` +anymore) + +* function `retry.Units` are removed + +* [more about this breaking change](https://github.com/avast/retry-go/issues/7) + +0.3.0 -> 1.0.0 + +* `retry.Retry` function are changed to `retry.Do` function + +* `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are +now implement via functions produces Options (aka `retry.OnRetry`) + +## Usage + +```go +var ( + DefaultAttempts = uint(10) + DefaultDelay = 100 * time.Millisecond + DefaultMaxJitter = 100 * time.Millisecond + DefaultOnRetry = func(n uint, err error) {} + DefaultRetryIf = IsRecoverable + DefaultDelayType = CombineDelay(BackOffDelay, RandomDelay) + DefaultLastErrorOnly = false + DefaultContext = context.Background() +) +``` + +#### func BackOffDelay + +```go +func BackOffDelay(n uint, _ error, config *Config) time.Duration +``` +BackOffDelay is a DelayType which increases delay between consecutive retries + +#### func Do + +```go +func Do(retryableFunc RetryableFunc, opts ...Option) error +``` + +#### func FixedDelay + +```go +func FixedDelay(_ uint, _ error, config *Config) time.Duration +``` +FixedDelay is a DelayType which keeps delay the same through all iterations + +#### func IsRecoverable + +```go +func IsRecoverable(err error) bool +``` +IsRecoverable checks if error is an instance of `unrecoverableError` + +#### func RandomDelay + +```go +func RandomDelay(_ uint, _ error, config *Config) time.Duration +``` +RandomDelay is a DelayType which picks a random delay up to config.maxJitter + +#### func Unrecoverable + +```go +func Unrecoverable(err error) error +``` +Unrecoverable wraps an error in `unrecoverableError` struct + +#### type Config + +```go +type Config struct { +} +``` + + +#### type DelayTypeFunc + +```go +type DelayTypeFunc func(n uint, err error, config *Config) time.Duration +``` + +DelayTypeFunc is called to return the next delay to wait after the retriable +function fails on `err` after `n` attempts. + +#### func CombineDelay + +```go +func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc +``` +CombineDelay is a DelayType the combines all of the specified delays into a new +DelayTypeFunc + +#### type Error + +```go +type Error []error +``` + +Error type represents list of errors in retry + +#### func (Error) Error + +```go +func (e Error) Error() string +``` +Error method return string representation of Error It is an implementation of +error interface + +#### func (Error) WrappedErrors + +```go +func (e Error) WrappedErrors() []error +``` +WrappedErrors returns the list of errors that this Error is wrapping. It is an +implementation of the `errwrap.Wrapper` interface in package +[errwrap](https://github.com/hashicorp/errwrap) so that `retry.Error` can be +used with that library. + +#### type OnRetryFunc + +```go +type OnRetryFunc func(n uint, err error) +``` + +Function signature of OnRetry function n = count of attempts + +#### type Option + +```go +type Option func(*Config) +``` + +Option represents an option for retry. + +#### func Attempts + +```go +func Attempts(attempts uint) Option +``` +Attempts set count of retry default is 10 + +#### func Context + +```go +func Context(ctx context.Context) Option +``` +Context allow to set context of retry default are Background context + +example of immediately cancellation (maybe it isn't the best example, but it +describes behavior enough; I hope) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + retry.Do( + func() error { + ... + }, + retry.Context(ctx), + ) + +#### func Delay + +```go +func Delay(delay time.Duration) Option +``` +Delay set delay between retry default is 100ms + +#### func DelayType + +```go +func DelayType(delayType DelayTypeFunc) Option +``` +DelayType set type of the delay between retries default is BackOff + +#### func LastErrorOnly + +```go +func LastErrorOnly(lastErrorOnly bool) Option +``` +return the direct last error that came from the retried function default is +false (return wrapped errors with everything) + +#### func MaxDelay + +```go +func MaxDelay(maxDelay time.Duration) Option +``` +MaxDelay set maximum delay between retry does not apply by default + +#### func MaxJitter + +```go +func MaxJitter(maxJitter time.Duration) Option +``` +MaxJitter sets the maximum random Jitter between retries for RandomDelay + +#### func OnRetry + +```go +func OnRetry(onRetry OnRetryFunc) Option +``` +OnRetry function callback are called each retry + +log each retry example: + + retry.Do( + func() error { + return errors.New("some error") + }, + retry.OnRetry(func(n uint, err error) { + log.Printf("#%d: %s\n", n, err) + }), + ) + +#### func RetryIf + +```go +func RetryIf(retryIf RetryIfFunc) Option +``` +RetryIf controls whether a retry should be attempted after an error (assuming +there are any retry attempts remaining) + +skip retry if special error example: + + retry.Do( + func() error { + return errors.New("special error") + }, + retry.RetryIf(func(err error) bool { + if err.Error() == "special error" { + return false + } + return true + }) + ) + +By default RetryIf stops execution if the error is wrapped using +`retry.Unrecoverable`, so above example may also be shortened to: + + retry.Do( + func() error { + return retry.Unrecoverable(errors.New("special error")) + } + ) + +#### type RetryIfFunc + +```go +type RetryIfFunc func(error) bool +``` + +Function signature of retry if function + +#### type RetryableFunc + +```go +type RetryableFunc func() error +``` + +Function signature of retryable function + +## Contributing + +Contributions are very much welcome. + +### Makefile + +Makefile provides several handy rules, like README.md `generator` , `setup` for prepare build/dev environment, `test`, `cover`, etc... + +Try `make help` for more information. + +### Before pull request + +please try: +* run tests (`make test`) +* run linter (`make lint`) +* if your IDE don't automaticaly do `go fmt`, run `go fmt` (`make fmt`) + +### README + +README.md are generate from template [.godocdown.tmpl](.godocdown.tmpl) and code documentation via [godocdown](https://github.com/robertkrimen/godocdown). + +Never edit README.md direct, because your change will be lost. diff --git a/vendor/github.com/avast/retry-go/VERSION b/vendor/github.com/avast/retry-go/VERSION new file mode 100644 index 0000000..4a36342 --- /dev/null +++ b/vendor/github.com/avast/retry-go/VERSION @@ -0,0 +1 @@ +3.0.0 diff --git a/vendor/github.com/avast/retry-go/appveyor.yml b/vendor/github.com/avast/retry-go/appveyor.yml new file mode 100644 index 0000000..dc5234a --- /dev/null +++ b/vendor/github.com/avast/retry-go/appveyor.yml @@ -0,0 +1,19 @@ +version: "{build}" + +clone_folder: c:\Users\appveyor\go\src\github.com\avast\retry-go + +#os: Windows Server 2012 R2 +platform: x64 + +install: + - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe + - set GOPATH=C:\Users\appveyor\go + - set PATH=%PATH%;c:\MinGW\bin + - set PATH=%PATH%;%GOPATH%\bin;c:\go\bin + - set GOBIN=%GOPATH%\bin + - go version + - go env + - make setup + +build_script: + - make ci diff --git a/vendor/github.com/avast/retry-go/options.go b/vendor/github.com/avast/retry-go/options.go new file mode 100644 index 0000000..a6c5720 --- /dev/null +++ b/vendor/github.com/avast/retry-go/options.go @@ -0,0 +1,198 @@ +package retry + +import ( + "context" + "math" + "math/rand" + "time" +) + +// Function signature of retry if function +type RetryIfFunc func(error) bool + +// Function signature of OnRetry function +// n = count of attempts +type OnRetryFunc func(n uint, err error) + +// DelayTypeFunc is called to return the next delay to wait after the retriable function fails on `err` after `n` attempts. +type DelayTypeFunc func(n uint, err error, config *Config) time.Duration + +type Config struct { + attempts uint + delay time.Duration + maxDelay time.Duration + maxJitter time.Duration + onRetry OnRetryFunc + retryIf RetryIfFunc + delayType DelayTypeFunc + lastErrorOnly bool + context context.Context + + maxBackOffN uint +} + +// Option represents an option for retry. +type Option func(*Config) + +// return the direct last error that came from the retried function +// default is false (return wrapped errors with everything) +func LastErrorOnly(lastErrorOnly bool) Option { + return func(c *Config) { + c.lastErrorOnly = lastErrorOnly + } +} + +// Attempts set count of retry +// default is 10 +func Attempts(attempts uint) Option { + return func(c *Config) { + c.attempts = attempts + } +} + +// Delay set delay between retry +// default is 100ms +func Delay(delay time.Duration) Option { + return func(c *Config) { + c.delay = delay + } +} + +// MaxDelay set maximum delay between retry +// does not apply by default +func MaxDelay(maxDelay time.Duration) Option { + return func(c *Config) { + c.maxDelay = maxDelay + } +} + +// MaxJitter sets the maximum random Jitter between retries for RandomDelay +func MaxJitter(maxJitter time.Duration) Option { + return func(c *Config) { + c.maxJitter = maxJitter + } +} + +// DelayType set type of the delay between retries +// default is BackOff +func DelayType(delayType DelayTypeFunc) Option { + return func(c *Config) { + c.delayType = delayType + } +} + +// BackOffDelay is a DelayType which increases delay between consecutive retries +func BackOffDelay(n uint, _ error, config *Config) time.Duration { + // 1 << 63 would overflow signed int64 (time.Duration), thus 62. + const max uint = 62 + + if config.maxBackOffN == 0 { + if config.delay <= 0 { + config.delay = 1 + } + + config.maxBackOffN = max - uint(math.Floor(math.Log2(float64(config.delay)))) + } + + if n > config.maxBackOffN { + n = config.maxBackOffN + } + + return config.delay << n +} + +// FixedDelay is a DelayType which keeps delay the same through all iterations +func FixedDelay(_ uint, _ error, config *Config) time.Duration { + return config.delay +} + +// RandomDelay is a DelayType which picks a random delay up to config.maxJitter +func RandomDelay(_ uint, _ error, config *Config) time.Duration { + return time.Duration(rand.Int63n(int64(config.maxJitter))) +} + +// CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc +func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc { + const maxInt64 = uint64(math.MaxInt64) + + return func(n uint, err error, config *Config) time.Duration { + var total uint64 + for _, delay := range delays { + total += uint64(delay(n, err, config)) + if total > maxInt64 { + total = maxInt64 + } + } + + return time.Duration(total) + } +} + +// OnRetry function callback are called each retry +// +// log each retry example: +// +// retry.Do( +// func() error { +// return errors.New("some error") +// }, +// retry.OnRetry(func(n uint, err error) { +// log.Printf("#%d: %s\n", n, err) +// }), +// ) +func OnRetry(onRetry OnRetryFunc) Option { + return func(c *Config) { + c.onRetry = onRetry + } +} + +// RetryIf controls whether a retry should be attempted after an error +// (assuming there are any retry attempts remaining) +// +// skip retry if special error example: +// +// retry.Do( +// func() error { +// return errors.New("special error") +// }, +// retry.RetryIf(func(err error) bool { +// if err.Error() == "special error" { +// return false +// } +// return true +// }) +// ) +// +// By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`, +// so above example may also be shortened to: +// +// retry.Do( +// func() error { +// return retry.Unrecoverable(errors.New("special error")) +// } +// ) +func RetryIf(retryIf RetryIfFunc) Option { + return func(c *Config) { + c.retryIf = retryIf + } +} + +// Context allow to set context of retry +// default are Background context +// +// example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope) +// +// ctx, cancel := context.WithCancel(context.Background()) +// cancel() +// +// retry.Do( +// func() error { +// ... +// }, +// retry.Context(ctx), +// ) +func Context(ctx context.Context) Option { + return func(c *Config) { + c.context = ctx + } +} diff --git a/vendor/github.com/avast/retry-go/retry.go b/vendor/github.com/avast/retry-go/retry.go new file mode 100644 index 0000000..af2d926 --- /dev/null +++ b/vendor/github.com/avast/retry-go/retry.go @@ -0,0 +1,225 @@ +/* +Simple library for retry mechanism + +slightly inspired by [Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry) + +SYNOPSIS + +http get with retry: + + url := "http://example.com" + var body []byte + + err := retry.Do( + func() error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return nil + }, + ) + + fmt.Println(body) + +[next examples](https://github.com/avast/retry-go/tree/master/examples) + + +SEE ALSO + +* [giantswarm/retry-go](https://github.com/giantswarm/retry-go) - slightly complicated interface. + +* [sethgrid/pester](https://github.com/sethgrid/pester) - only http retry for http calls with retries and backoff + +* [cenkalti/backoff](https://github.com/cenkalti/backoff) - Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java. Really complicated interface. + +* [rafaeljesus/retry-go](https://github.com/rafaeljesus/retry-go) - looks good, slightly similar as this package, don't have 'simple' `Retry` method + +* [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me) + + +BREAKING CHANGES + +3.0.0 + +* `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go). + + +1.0.2 -> 2.0.0 + +* argument of `retry.Delay` is final delay (no multiplication by `retry.Units` anymore) + +* function `retry.Units` are removed + +* [more about this breaking change](https://github.com/avast/retry-go/issues/7) + + +0.3.0 -> 1.0.0 + +* `retry.Retry` function are changed to `retry.Do` function + +* `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`) + + +*/ +package retry + +import ( + "context" + "fmt" + "strings" + "time" +) + +// Function signature of retryable function +type RetryableFunc func() error + +var ( + DefaultAttempts = uint(10) + DefaultDelay = 100 * time.Millisecond + DefaultMaxJitter = 100 * time.Millisecond + DefaultOnRetry = func(n uint, err error) {} + DefaultRetryIf = IsRecoverable + DefaultDelayType = CombineDelay(BackOffDelay, RandomDelay) + DefaultLastErrorOnly = false + DefaultContext = context.Background() +) + +func Do(retryableFunc RetryableFunc, opts ...Option) error { + var n uint + + //default + config := &Config{ + attempts: DefaultAttempts, + delay: DefaultDelay, + maxJitter: DefaultMaxJitter, + onRetry: DefaultOnRetry, + retryIf: DefaultRetryIf, + delayType: DefaultDelayType, + lastErrorOnly: DefaultLastErrorOnly, + context: DefaultContext, + } + + //apply opts + for _, opt := range opts { + opt(config) + } + + if err := config.context.Err(); err != nil { + return err + } + + var errorLog Error + if !config.lastErrorOnly { + errorLog = make(Error, config.attempts) + } else { + errorLog = make(Error, 1) + } + + lastErrIndex := n + for n < config.attempts { + err := retryableFunc() + + if err != nil { + errorLog[lastErrIndex] = unpackUnrecoverable(err) + + if !config.retryIf(err) { + break + } + + config.onRetry(n, err) + + // if this is last attempt - don't wait + if n == config.attempts-1 { + break + } + + delayTime := config.delayType(n, err, config) + if config.maxDelay > 0 && delayTime > config.maxDelay { + delayTime = config.maxDelay + } + + select { + case <-time.After(delayTime): + case <-config.context.Done(): + return config.context.Err() + } + + } else { + return nil + } + + n++ + if !config.lastErrorOnly { + lastErrIndex = n + } + } + + if config.lastErrorOnly { + return errorLog[lastErrIndex] + } + return errorLog +} + +// Error type represents list of errors in retry +type Error []error + +// Error method return string representation of Error +// It is an implementation of error interface +func (e Error) Error() string { + logWithNumber := make([]string, lenWithoutNil(e)) + for i, l := range e { + if l != nil { + logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error()) + } + } + + return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logWithNumber, "\n")) +} + +func lenWithoutNil(e Error) (count int) { + for _, v := range e { + if v != nil { + count++ + } + } + + return +} + +// WrappedErrors returns the list of errors that this Error is wrapping. +// It is an implementation of the `errwrap.Wrapper` interface +// in package [errwrap](https://github.com/hashicorp/errwrap) so that +// `retry.Error` can be used with that library. +func (e Error) WrappedErrors() []error { + return e +} + +type unrecoverableError struct { + error +} + +// Unrecoverable wraps an error in `unrecoverableError` struct +func Unrecoverable(err error) error { + return unrecoverableError{err} +} + +// IsRecoverable checks if error is an instance of `unrecoverableError` +func IsRecoverable(err error) bool { + _, isUnrecoverable := err.(unrecoverableError) + return !isUnrecoverable +} + +func unpackUnrecoverable(err error) error { + if unrecoverable, isUnrecoverable := err.(unrecoverableError); isUnrecoverable { + return unrecoverable.error + } + + return err +} diff --git a/vendor/github.com/tyzbit/go-archive/main.go b/vendor/github.com/tyzbit/go-archive/main.go index 1600ff2..4213126 100644 --- a/vendor/github.com/tyzbit/go-archive/main.go +++ b/vendor/github.com/tyzbit/go-archive/main.go @@ -7,6 +7,9 @@ import ( "net/http" "regexp" "strings" + "time" + + "github.com/avast/retry-go" ) const ( @@ -29,40 +32,55 @@ type ArchiveOrgWaybackResponse struct { // Gets the most recent archive.org URL for a url and a boolean whether or not to // archive the page if not found. Returns the latest archive.org URL for the page // and a boolean whether or not the page existed -func GetLatestURL(url string) (archiveUrl string, exists bool, err error) { +func GetLatestURL(url string, retryAttempts uint) (archiveUrl string, exists bool, err error) { + resp := http.Response{} + // This obliterates the `http` namespace so it must come after + // creating the response object. http := http.Client{} - resp, err := http.Get(archiveApi + "/wayback/available?url=" + url) - if err != nil { - return "", false, fmt.Errorf("error calling wayback api: %w", err) - } + if err := retry.Do(func() error { + resp, err := http.Get(archiveApi + "/wayback/available?url=" + url) + if err != nil { + return fmt.Errorf("error calling wayback api: %w", err) + } + if resp.StatusCode == 429 { + return fmt.Errorf("rate limited by wayback api") + } + return nil + }, + retry.Attempts(retryAttempts), + retry.Delay(1*time.Second), + retry.DelayType(retry.BackOffDelay), + ); err != nil { + return "", false, fmt.Errorf("all %d attempts failed: %w", retryAttempts, err) + } else { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", false, fmt.Errorf("error reading body from wayback api: %w", err) + } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", false, fmt.Errorf("error reading body from wayback api: %w", err) - } + var r ArchiveOrgWaybackResponse + err = json.Unmarshal(body, &r) + if err != nil { + return "", false, fmt.Errorf("error unmarshalling json: %w, body: %v", err, string(body)) + } - var r ArchiveOrgWaybackResponse - err = json.Unmarshal(body, &r) - if err != nil { - return "", false, fmt.Errorf("error unmarshalling json: %w, body: %v", err, string(body)) - } + if r.ArchivedSnapshots.Closest.URL == "" { + return "", false, nil + } - if r.ArchivedSnapshots.Closest.URL == "" { - return "", false, nil + return r.ArchivedSnapshots.Closest.URL, true, nil } - - return r.ArchivedSnapshots.Closest.URL, true, nil } // Takes a slice of strings and a boolean whether or not to archive the page if not found // and returns a slice of strings of archive.org URLs and any errors. -func GetLatestURLs(urls []string, archiveIfNotFound bool) (archiveUrls []string, errs []error) { +func GetLatestURLs(urls []string, retryAttempts uint, archiveIfNotFound bool) (archiveUrls []string, errs []error) { var errors []error var response []string for _, url := range urls { var err error - archiveUrl, exists, err := GetLatestURL(url) + archiveUrl, exists, err := GetLatestURL(url, retryAttempts) if err != nil { errors = append(errors, fmt.Errorf("unable to get latest archive URL for %v, we got: %v, err: %w", url, archiveUrl, err)) continue diff --git a/vendor/modules.txt b/vendor/modules.txt index 497d5a5..35f53ac 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,6 +2,9 @@ ## explicit; go 1.16 github.com/BurntSushi/toml github.com/BurntSushi/toml/internal +# github.com/avast/retry-go v3.0.0+incompatible +## explicit +github.com/avast/retry-go # github.com/bwmarrin/discordgo v0.25.0 ## explicit; go 1.13 github.com/bwmarrin/discordgo @@ -81,7 +84,7 @@ github.com/mvdan/xurls # github.com/sirupsen/logrus v1.8.1 ## explicit; go 1.13 github.com/sirupsen/logrus -# github.com/tyzbit/go-archive v0.0.0-20220422014241-aa077f0a8174 +# github.com/tyzbit/go-archive v0.0.0-20230206195843-20514a4efc44 ## explicit; go 1.18 github.com/tyzbit/go-archive # github.com/ugorji/go/codec v1.1.7