From 0ae34112b691653de43c4c4780765b2a4e5884b8 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sat, 3 Apr 2021 14:11:47 +0200 Subject: [PATCH] Add code linting / binary publishing (#3) --- .golangci.yml | 59 +++++++++++ .repo-runner.yaml | 13 +++ Makefile | 14 ++- action_script.go | 2 +- irc.go | 5 +- main.go | 13 ++- msgformatter.go | 2 +- rule.go | 246 +++++++++++++++++++++++++++++----------------- rule_test.go | 164 +++++++++++++++++++++++++++++++ timers.go | 2 - twitch.go | 6 +- 11 files changed, 426 insertions(+), 100 deletions(-) create mode 100644 .golangci.yml create mode 100644 .repo-runner.yaml create mode 100644 rule_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..f78879c --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,59 @@ +# Derived from https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml + +--- + +run: + skip-dirs: + - config + skip-files: + - assets.go + - bindata.go + +output: + format: tab + +linters-settings: + funlen: + lines: 100 + statements: 60 + + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 15 + +linters: + disable-all: true + enable: + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] + - bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false] + - deadcode # Finds unused code [fast: true, auto-fix: false] + - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] + - exportloopref # checks for pointers to enclosing loop variables [fast: true, auto-fix: false] + - funlen # Tool for detection of long functions [fast: true, auto-fix: false] + - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] + - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] + - gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false] + - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] + - godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] + - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] + - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] + - gosec # Inspects source code for security problems [fast: true, auto-fix: false] + - gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false] + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false] + - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] + - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] + - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] + - noctx # noctx finds sending http request without context.Context [fast: true, auto-fix: false] + - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false] + - structcheck # Finds unused struct fields [fast: true, auto-fix: false] + - stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false] + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false] + - unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false] + - unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] + - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] + +... diff --git a/.repo-runner.yaml b/.repo-runner.yaml new file mode 100644 index 0000000..033d57a --- /dev/null +++ b/.repo-runner.yaml @@ -0,0 +1,13 @@ +--- + +image: "reporunner/golang-alpine" +checkout_dir: /go/src/github.com/Luzifer/twitch-bot + +commands: + - make lint test publish + +environment: + DRAFT: "false" + CGO_ENABLED: 0 + GO111MODULE: on + MOD_MODE: readonly diff --git a/Makefile b/Makefile index 665b645..4c1b6bf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,16 @@ -default: +default: lint test + +lint: + golangci-lint run --timeout=5m + +publish: + curl -sSLo golang.sh https://raw.githubusercontent.com/Luzifer/github-publish/master/golang.sh + bash golang.sh + +test: + go test -cover -v . + +# --- Wiki Updates pull_wiki: git subtree pull --prefix=wiki https://github.com/Luzifer/twitch-bot.wiki.git master --squash diff --git a/action_script.go b/action_script.go index b29b360..32ba67c 100644 --- a/action_script.go +++ b/action_script.go @@ -45,7 +45,7 @@ func init() { return errors.Wrap(err, "encoding script input") } - cmd := exec.CommandContext(ctx, command[0], command[1:]...) + cmd := exec.CommandContext(ctx, command[0], command[1:]...) // #nosec G204 // This is expected to call a command with parameters cmd.Env = os.Environ() cmd.Stderr = os.Stderr cmd.Stdin = stdin diff --git a/irc.go b/irc.go index 368f7d1..e8a1881 100644 --- a/irc.go +++ b/irc.go @@ -19,7 +19,6 @@ const ( badgeFounder = "founder" badgeModerator = "moderator" badgeSubscriber = "subscriber" - badgeVIP = "vip" ) type ircHandler struct { @@ -135,7 +134,7 @@ func (i ircHandler) handlePermit(m *irc.Message) { } msgParts := strings.Split(m.Trailing(), " ") - if len(msgParts) != 2 { + if len(msgParts) != 2 { //nolint:gomnd // This is not a magic number but just an expected count return } @@ -230,7 +229,7 @@ func (ircHandler) ParseBadgeLevels(m *irc.Message) badgeCollection { badges := strings.Split(badgeString, ",") for _, b := range badges { badgeParts := strings.Split(b, "/") - if len(badgeParts) != 2 { + if len(badgeParts) != 2 { //nolint:gomnd // This is not a magic number but just an expected count log.WithField("badge", b).Warn("Malformed badge found") continue } diff --git a/main.go b/main.go index 3daf39b..e80ff96 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "strings" "sync" "time" @@ -12,6 +13,8 @@ import ( "github.com/Luzifer/rconfig/v2" ) +const ircReconnectDelay = 100 * time.Millisecond + var ( cfg = struct { CommandTimeout time.Duration `flag:"command-timeout" default:"30s" description:"Timeout for command execution"` @@ -32,6 +35,13 @@ var ( ) func init() { + for _, a := range os.Args { + if strings.HasPrefix(a, "-test.") { + // Skip initialize for test run + return + } + } + rconfig.AutoEnv(true) if err := rconfig.ParseAndValidate(&cfg); err != nil { log.Fatalf("Unable to parse commandline options: %s", err) @@ -49,6 +59,7 @@ func init() { } } +//nolint: gocognit,gocyclo // Complexity is a little too high but makes no sense to split func main() { var err error @@ -93,7 +104,7 @@ func main() { if err := irc.Run(); err != nil { log.WithError(err).Error("IRC run exited unexpectedly") } - time.Sleep(100 * time.Millisecond) + time.Sleep(ircReconnectDelay) ircDisconnected <- struct{}{} }() diff --git a/msgformatter.go b/msgformatter.go index 5a0cbec..77b50ec 100644 --- a/msgformatter.go +++ b/msgformatter.go @@ -86,7 +86,7 @@ func formatMessage(tplString string, m *irc.Message, r *rule, fields map[string] } fields["msg"] = m - fields["permitTimeout"] = int64(*&config.PermitTimeout / time.Second) + fields["permitTimeout"] = int64(config.PermitTimeout / time.Second) fields["username"] = m.User if m.Command == "PRIVMSG" && len(m.Params) > 0 { diff --git a/rule.go b/rule.go index 0d39232..3d71a20 100644 --- a/rule.go +++ b/rule.go @@ -51,8 +51,6 @@ func (r rule) MatcherID() string { } func (r *rule) Matches(m *irc.Message, event *string) bool { - var err error - var ( badges = ircHandler{}.ParseBadgeLevels(m) logger = log.WithFields(log.Fields{ @@ -61,69 +59,28 @@ func (r *rule) Matches(m *irc.Message, event *string) bool { }) ) - // Check Channel match - if len(r.MatchChannels) > 0 { - if len(m.Params) == 0 || !str.StringInSlice(m.Params[0], r.MatchChannels) { - logger.Trace("Non-Match: Channel") + for _, matcher := range []func(*log.Entry, *irc.Message, *string, badgeCollection) bool{ + r.allowExecuteChannelWhitelist, + r.allowExecuteUserWhitelist, + r.allowExecuteEventWhitelist, + r.allowExecuteMessageMatcherWhitelist, + r.allowExecuteMessageMatcherBlacklist, + r.allowExecuteBadgeBlacklist, + r.allowExecuteBadgeWhitelist, + r.allowExecuteDisableOnPermit, + r.allowExecuteCooldown, + r.allowExecuteDisableOnOffline, + } { + if !matcher(logger, m, event, badges) { return false } } - if len(r.MatchUsers) > 0 { - if !str.StringInSlice(strings.ToLower(m.User), r.MatchUsers) { - logger.Trace("Non-Match: Users") - return false - } - } + // Nothing objected: Matches! + return true +} - // Check Event match - if r.MatchEvent != nil { - if event == nil || *r.MatchEvent != *event { - logger.Trace("Non-Match: Event") - return false - } - } - - // Check Message match - if r.MatchMessage != nil { - // If the regexp was not yet compiled, cache it - if r.matchMessage == nil { - if r.matchMessage, err = regexp.Compile(*r.MatchMessage); err != nil { - logger.WithError(err).Error("Unable to compile expression") - return false - } - } - - // Check whether the message matches - if !r.matchMessage.MatchString(m.Trailing()) { - logger.Trace("Non-Match: Message") - return false - } - } - - if len(r.DisableOnMatchMessages) > 0 { - // If the regexps were not pre-compiled, do it now - if len(r.disableOnMatchMessages) != len(r.DisableOnMatchMessages) { - r.disableOnMatchMessages = nil - for _, dm := range r.DisableOnMatchMessages { - dmr, err := regexp.Compile(dm) - if err != nil { - logger.WithError(err).Error("Unable to compile expression") - return false - } - r.disableOnMatchMessages = append(r.disableOnMatchMessages, dmr) - } - } - - for _, rex := range r.disableOnMatchMessages { - if rex.MatchString(m.Trailing()) { - logger.Trace("Non-Match: Disable-On-Message") - return false - } - } - } - - // Check whether user has one of the disable rules +func (r *rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { for _, b := range r.DisableOn { if badges.Has(b) { logger.Tracef("Non-Match: Disable-Badge %s", b) @@ -131,53 +88,164 @@ func (r *rule) Matches(m *irc.Message, event *string) bool { } } - // Check whether user has at least one of the enable rules - if len(r.EnableOn) > 0 { - var userHasEnableBadge bool - for _, b := range r.EnableOn { - if badges.Has(b) { - userHasEnableBadge = true - } - } - if !userHasEnableBadge { - logger.Trace("Non-Match: No enable-badges") - return false + return true +} + +func (r *rule) allowExecuteBadgeWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if len(r.EnableOn) == 0 { + // No match criteria set, does not speak against matching + return true + } + + for _, b := range r.EnableOn { + if badges.Has(b) { + return true } } - // Check on permit + return false +} + +func (r *rule) allowExecuteChannelWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if len(r.MatchChannels) == 0 { + // No match criteria set, does not speak against matching + return true + } + + if len(m.Params) == 0 || (!str.StringInSlice(m.Params[0], r.MatchChannels) && !str.StringInSlice(strings.TrimPrefix(m.Params[0], "#"), r.MatchChannels)) { + logger.Trace("Non-Match: Channel") + return false + } + + return true +} + +func (r *rule) allowExecuteCooldown(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if r.Cooldown == nil { + // No match criteria set, does not speak against matching + return true + } + + if !timerStore.InCooldown(r.MatcherID(), *r.Cooldown) { + return true + } + + for _, b := range r.SkipCooldownFor { + if badges.Has(b) { + return true + } + } + + return false +} + +func (r *rule) allowExecuteDisableOnOffline(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if !r.DisableOnOffline { + // No match criteria set, does not speak against matching + return true + } + + streamLive, err := twitch.HasLiveStream(strings.TrimLeft(m.Params[0], "#")) + if err != nil { + logger.WithError(err).Error("Unable to determine live status") + return false + } + if !streamLive { + logger.Trace("Non-Match: Stream offline") + return false + } + + return true +} + +func (r *rule) allowExecuteDisableOnPermit(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { if r.DisableOnPermit && timerStore.HasPermit(m.Params[0], m.User) { logger.Trace("Non-Match: Permit") return false } - // Check whether rule is in cooldown - if r.Cooldown != nil && timerStore.InCooldown(r.MatcherID(), *r.Cooldown) { - var userHasSkipBadge bool - for _, b := range r.SkipCooldownFor { - if badges.Has(b) { - userHasSkipBadge = true + return true +} + +func (r *rule) allowExecuteEventWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if r.MatchEvent == nil { + // No match criteria set, does not speak against matching + return true + } + + if event == nil || *r.MatchEvent != *event { + logger.Trace("Non-Match: Event") + return false + } + + return true +} + +func (r *rule) allowExecuteMessageMatcherBlacklist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if len(r.DisableOnMatchMessages) == 0 { + // No match criteria set, does not speak against matching + return true + } + + // If the regexps were not pre-compiled, do it now + if len(r.disableOnMatchMessages) != len(r.DisableOnMatchMessages) { + r.disableOnMatchMessages = nil + for _, dm := range r.DisableOnMatchMessages { + dmr, err := regexp.Compile(dm) + if err != nil { + logger.WithError(err).Error("Unable to compile expression") + return false } + r.disableOnMatchMessages = append(r.disableOnMatchMessages, dmr) } - if !userHasSkipBadge { - logger.Trace("Non-Match: On cooldown") + } + + for _, rex := range r.disableOnMatchMessages { + if rex.MatchString(m.Trailing()) { + logger.Trace("Non-Match: Disable-On-Message") return false } } - if r.DisableOnOffline { - streamLive, err := twitch.HasLiveStream(strings.TrimLeft(m.Params[0], "#")) - if err != nil { - logger.WithError(err).Error("Unable to determine live status") - return false - } - if !streamLive { - logger.Trace("Non-Match: Stream offline") + return true +} + +func (r *rule) allowExecuteMessageMatcherWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if r.MatchMessage == nil { + // No match criteria set, does not speak against matching + return true + } + + var err error + + // If the regexp was not yet compiled, cache it + if r.matchMessage == nil { + if r.matchMessage, err = regexp.Compile(*r.MatchMessage); err != nil { + logger.WithError(err).Error("Unable to compile expression") return false } } - // Nothing objected: Matches! + // Check whether the message matches + if !r.matchMessage.MatchString(m.Trailing()) { + logger.Trace("Non-Match: Message") + return false + } + + return true +} + +func (r *rule) allowExecuteUserWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { + if len(r.MatchUsers) == 0 { + // No match criteria set, does not speak against matching + return true + } + + if !str.StringInSlice(strings.ToLower(m.User), r.MatchUsers) { + logger.Trace("Non-Match: Users") + return false + } + return true } diff --git a/rule_test.go b/rule_test.go new file mode 100644 index 0000000..de84c96 --- /dev/null +++ b/rule_test.go @@ -0,0 +1,164 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/go-irc/irc" + "github.com/sirupsen/logrus" +) + +var ( + testLogger = logrus.NewEntry(logrus.StandardLogger()) + testBadgeLevel0 = func(i int) *int { return &i }(0) +) + +func TestAllowExecuteBadgeBlacklist(t *testing.T) { + r := &rule{DisableOn: []string{badgeBroadcaster}} + + if r.allowExecuteBadgeBlacklist(testLogger, nil, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Execution allowed on blacklisted badge") + } + + if !r.allowExecuteBadgeBlacklist(testLogger, nil, nil, badgeCollection{badgeModerator: testBadgeLevel0}) { + t.Error("Execution denied without blacklisted badge") + } +} + +func TestAllowExecuteBadgeWhitelist(t *testing.T) { + r := &rule{EnableOn: []string{badgeBroadcaster}} + + if r.allowExecuteBadgeWhitelist(testLogger, nil, nil, badgeCollection{badgeModerator: testBadgeLevel0}) { + t.Error("Execution allowed without whitelisted badge") + } + + if !r.allowExecuteBadgeWhitelist(testLogger, nil, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Execution denied with whitelisted badge") + } +} + +func TestAllowExecuteChannelWhitelist(t *testing.T) { + r := &rule{MatchChannels: []string{"#mychannel", "otherchannel"}} + + for m, exp := range map[string]bool{ + ":amy!amy@foo.example.com PRIVMSG #mychannel :Testing": true, + ":amy!amy@foo.example.com PRIVMSG #otherchannel :Testing": true, + ":amy!amy@foo.example.com PRIVMSG #randomchannel :Testing": false, + ":amy!amy@foo.example.com JOIN #mychannel": true, + ":tmi.twitch.tv CLEARCHAT #mychannel": true, + ":tmi.twitch.tv CLEARCHAT #mychannel :ronni": true, + ":tmi.twitch.tv CLEARCHAT #dallas": false, + "@msg-id=slow_off :tmi.twitch.tv NOTICE #mychannel :This room is no longer in slow mode.": true, + } { + if res := r.allowExecuteChannelWhitelist(testLogger, irc.MustParseMessage(m), nil, badgeCollection{}); res != exp { + t.Errorf("Message %q yield unxpected result: exp=%v res=%v", m, exp, res) + } + } +} + +func TestAllowExecuteCooldown(t *testing.T) { + r := &rule{Cooldown: func(i time.Duration) *time.Duration { return &i }(time.Minute), SkipCooldownFor: []string{badgeBroadcaster}} + + if !r.allowExecuteCooldown(testLogger, nil, nil, badgeCollection{}) { + t.Error("Initial call was not allowed") + } + + // Add cooldown + timerStore.AddCooldown(r.MatcherID()) + + if r.allowExecuteCooldown(testLogger, nil, nil, badgeCollection{}) { + t.Error("Call after cooldown added was allowed") + } + + if !r.allowExecuteCooldown(testLogger, nil, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Call in cooldown with skip badge was not allowed") + } +} + +func TestAllowExecuteDisableOnOffline(t *testing.T) { + r := &rule{DisableOnOffline: true} + + // Fake cache entries to prevent calling the real Twitch API + twitch.apiCache.Set([]string{"hasLiveStream", "channel1"}, time.Minute, true) + twitch.apiCache.Set([]string{"hasLiveStream", "channel2"}, time.Minute, false) + + for ch, exp := range map[string]bool{ + "channel1": true, + "channel2": false, + } { + if res := r.allowExecuteDisableOnOffline(testLogger, irc.MustParseMessage(fmt.Sprintf("PRIVMSG #%s :test", ch)), nil, badgeCollection{}); res != exp { + t.Errorf("Channel %q yield an unexpected result: exp=%v res=%v", ch, exp, res) + } + } +} + +func TestAllowExecuteDisableOnPermit(t *testing.T) { + r := &rule{DisableOnPermit: true} + + // Permit is using global configuration, so we must fake that one + config = &configFile{PermitTimeout: time.Minute} + defer func() { config = nil }() + + m := irc.MustParseMessage(":amy!amy@foo.example.com PRIVMSG #mychannel :Testing") + if !r.allowExecuteDisableOnPermit(testLogger, m, nil, badgeCollection{}) { + t.Error("Execution was not allowed without permit") + } + + timerStore.AddPermit(m.Params[0], m.User) + if r.allowExecuteDisableOnPermit(testLogger, m, nil, badgeCollection{}) { + t.Error("Execution was allowed with permit") + } +} + +func TestAllowExecuteEventWhitelist(t *testing.T) { + r := &rule{MatchEvent: func(s string) *string { return &s }("test")} + + for evt, exp := range map[string]bool{ + "foobar": false, + "test": true, + } { + if res := r.allowExecuteEventWhitelist(testLogger, nil, &evt, badgeCollection{}); exp != res { + t.Errorf("Event %q yield unexpected result: exp=%v res=%v", evt, exp, res) + } + } +} + +func TestAllowExecuteMessageMatcherBlacklist(t *testing.T) { + r := &rule{DisableOnMatchMessages: []string{`^!disable`}} + + for msg, exp := range map[string]bool{ + "PRIVMSG #test :Random message": true, + "PRIVMSG #test :!disable this one": false, + } { + if res := r.allowExecuteMessageMatcherBlacklist(testLogger, irc.MustParseMessage(msg), nil, badgeCollection{}); exp != res { + t.Errorf("Message %q yield unexpected result: exp=%v res=%v", msg, exp, res) + } + } +} + +func TestAllowExecuteMessageMatcherWhitelist(t *testing.T) { + r := &rule{MatchMessage: func(s string) *string { return &s }(`^!test`)} + + for msg, exp := range map[string]bool{ + "PRIVMSG #test :Random message": false, + "PRIVMSG #test :!test this one": true, + } { + if res := r.allowExecuteMessageMatcherWhitelist(testLogger, irc.MustParseMessage(msg), nil, badgeCollection{}); exp != res { + t.Errorf("Message %q yield unexpected result: exp=%v res=%v", msg, exp, res) + } + } +} + +func TestAllowExecuteUserWhitelist(t *testing.T) { + r := &rule{MatchUsers: []string{"amy"}} + + for msg, exp := range map[string]bool{ + ":amy!amy@foo.example.com PRIVMSG #mychannel :Testing": true, + ":bob!bob@foo.example.com PRIVMSG #mychannel :Testing": false, + } { + if res := r.allowExecuteUserWhitelist(testLogger, irc.MustParseMessage(msg), nil, badgeCollection{}); exp != res { + t.Errorf("Message %q yield unexpected result: exp=%v res=%v", msg, exp, res) + } + } +} diff --git a/timers.go b/timers.go index 615865b..f4ca7d6 100644 --- a/timers.go +++ b/timers.go @@ -12,7 +12,6 @@ type timerType uint8 const ( timerTypePermit timerType = iota - timerTypeChatMessage timerTypeCooldown ) @@ -26,7 +25,6 @@ type timerEntry struct { type timer struct { timers map[string]timerEntry lock *sync.RWMutex - kind timerType } func newTimer() *timer { diff --git a/twitch.go b/twitch.go index 79cf064..108ee5e 100644 --- a/twitch.go +++ b/twitch.go @@ -12,6 +12,8 @@ import ( log "github.com/sirupsen/logrus" ) +const timeDay = 24 * time.Hour + var twitch = newTwitchClient() type twitchClient struct { @@ -85,7 +87,7 @@ func (t twitchClient) GetFollowDate(from, to string) (time.Time, error) { } // Follow date will not change that often, cache for a long time - t.apiCache.Set(cacheKey, 24*time.Hour, payload.Data[0].FollowedAt) + t.apiCache.Set(cacheKey, timeDay, payload.Data[0].FollowedAt) return payload.Data[0].FollowedAt, nil } @@ -154,7 +156,7 @@ func (t twitchClient) getIDForUsername(username string) (string, error) { } // The ID for an username will not change (often), cache for a long time - t.apiCache.Set(cacheKey, 24*time.Hour, payload.Data[0].ID) + t.apiCache.Set(cacheKey, timeDay, payload.Data[0].ID) return payload.Data[0].ID, nil }