Lint: Update linter config, improve code quality

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2024-01-01 17:52:18 +01:00
parent 7189232093
commit c78356f68f
Signed by: luzifer
SSH key fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
124 changed files with 1332 additions and 1062 deletions

View file

@ -12,34 +12,27 @@ output:
format: tab
issues:
# This disables the included exclude-list in golangci-lint as that
# list for example fully hides G304 gosec rule, errcheck, exported
# rule of revive and other errors one really wants to see.
# Smme detail: https://github.com/golangci/golangci-lint/issues/456
exclude-use-default: false
# Don't limit the number of shown issues: Report ALL of them
max-issues-per-linter: 0
max-same-issues: 0
linters-settings:
forbidigo:
forbid:
- 'fmt\.Errorf' # Should use github.com/pkg/errors
funlen:
lines: 100
statements: 60
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 15
gomnd:
settings:
mnd:
ignored-functions: 'strconv.(?:Format|Parse)\B+'
linters:
disable-all: true
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
- bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false]
- contextcheck # check the function whether use a non-inherited context [fast: false, auto-fix: false]
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
- exportloopref # checks for pointers to enclosing loop variables [fast: true, auto-fix: false]
- forbidigo # Forbids identifiers [fast: true, auto-fix: false]
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
@ -58,13 +51,124 @@ linters:
- 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]
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, 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]
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
- stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false]
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, 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]
- wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
linters-settings:
funlen:
lines: 100
statements: 60
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 15
gomnd:
settings:
mnd:
ignored-functions: 'strconv.(?:Format|Parse)\B+'
revive:
rules:
#- name: add-constant # Suggests using constant for magic numbers and string literals
# Opinion: Makes sense for strings, not for numbers but checks numbers
#- name: argument-limit # Specifies the maximum number of arguments a function can receive | Opinion: Don't need this
- name: atomic # Check for common mistaken usages of the `sync/atomic` package
- name: banned-characters # Checks banned characters in identifiers
arguments:
- ';' # Greek question mark
- name: bare-return # Warns on bare returns
- name: blank-imports # Disallows blank imports
- name: bool-literal-in-expr # Suggests removing Boolean literals from logic expressions
- name: call-to-gc # Warns on explicit call to the garbage collector
#- name: cognitive-complexity # Sets restriction for maximum Cognitive complexity.
# There is a dedicated linter for this
- name: confusing-naming # Warns on methods with names that differ only by capitalization
- name: confusing-results # Suggests to name potentially confusing function results
- name: constant-logical-expr # Warns on constant logical expressions
- name: context-as-argument # `context.Context` should be the first argument of a function.
- name: context-keys-type # Disallows the usage of basic types in `context.WithValue`.
#- name: cyclomatic # Sets restriction for maximum Cyclomatic complexity.
# There is a dedicated linter for this
#- name: datarace # Spots potential dataraces
# Is not (yet) available?
- name: deep-exit # Looks for program exits in funcs other than `main()` or `init()`
- name: defer # Warns on some [defer gotchas](https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1)
- name: dot-imports # Forbids `.` imports.
- name: duplicated-imports # Looks for packages that are imported two or more times
- name: early-return # Spots if-then-else statements that can be refactored to simplify code reading
- name: empty-block # Warns on empty code blocks
- name: empty-lines # Warns when there are heading or trailing newlines in a block
- name: errorf # Should replace `errors.New(fmt.Sprintf())` with `fmt.Errorf()`
- name: error-naming # Naming of error variables.
- name: error-return # The error return parameter should be last.
- name: error-strings # Conventions around error strings.
- name: exported # Naming and commenting conventions on exported symbols.
arguments: ['sayRepetitiveInsteadOfStutters']
#- name: file-header # Header which each file should have.
# Useless without config, have no config for it
- name: flag-parameter # Warns on boolean parameters that create a control coupling
#- name: function-length # Warns on functions exceeding the statements or lines max
# There is a dedicated linter for this
#- name: function-result-limit # Specifies the maximum number of results a function can return
# Opinion: Don't need this
- name: get-return # Warns on getters that do not yield any result
- name: identical-branches # Spots if-then-else statements with identical `then` and `else` branches
- name: if-return # Redundant if when returning an error.
#- name: imports-blacklist # Disallows importing the specified packages
# Useless without config, have no config for it
- name: import-shadowing # Spots identifiers that shadow an import
- name: increment-decrement # Use `i++` and `i--` instead of `i += 1` and `i -= 1`.
- name: indent-error-flow # Prevents redundant else statements.
#- name: line-length-limit # Specifies the maximum number of characters in a lined
# There is a dedicated linter for this
#- name: max-public-structs # The maximum number of public structs in a file.
# Opinion: Don't need this
- name: modifies-parameter # Warns on assignments to function parameters
- name: modifies-value-receiver # Warns on assignments to value-passed method receivers
#- name: nested-structs # Warns on structs within structs
# Opinion: Don't need this
- name: optimize-operands-order # Checks inefficient conditional expressions
#- name: package-comments # Package commenting conventions.
# Opinion: Don't need this
- name: range # Prevents redundant variables when iterating over a collection.
- name: range-val-address # Warns if address of range value is used dangerously
- name: range-val-in-closure # Warns if range value is used in a closure dispatched as goroutine
- name: receiver-naming # Conventions around the naming of receivers.
- name: redefines-builtin-id # Warns on redefinitions of builtin identifiers
#- name: string-format # Warns on specific string literals that fail one or more user-configured regular expressions
# Useless without config, have no config for it
- name: string-of-int # Warns on suspicious casts from int to string
- name: struct-tag # Checks common struct tags like `json`,`xml`,`yaml`
- name: superfluous-else # Prevents redundant else statements (extends indent-error-flow)
- name: time-equal # Suggests to use `time.Time.Equal` instead of `==` and `!=` for equality check time.
- name: time-naming # Conventions around the naming of time variables.
- name: unconditional-recursion # Warns on function calls that will lead to (direct) infinite recursion
- name: unexported-naming # Warns on wrongly named un-exported symbols
- name: unexported-return # Warns when a public return is from unexported type.
- name: unhandled-error # Warns on unhandled errors returned by funcion calls
arguments:
- "fmt.(Fp|P)rint(f|ln|)"
- name: unnecessary-stmt # Suggests removing or simplifying unnecessary statements
- name: unreachable-code # Warns on unreachable code
- name: unused-parameter # Suggests to rename or remove unused function parameters
- name: unused-receiver # Suggests to rename or remove unused method receivers
#- name: use-any # Proposes to replace `interface{}` with its alias `any`
# Is not (yet) available?
- name: useless-break # Warns on useless `break` statements in case clauses
- name: var-declaration # Reduces redundancies around variable declaration.
- name: var-naming # Naming rules.
- name: waitgroup-by-value # Warns on functions taking sync.WaitGroup as a by-value parameter
...

View file

@ -45,8 +45,10 @@ func init() {
})
}
// ActorScript contains an actor to execute arbitrary commands and scripts
type ActorScript struct{}
// Execute implements actor interface
func (ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
command, err := attrs.StringSlice("command")
if err != nil {
@ -121,9 +123,13 @@ func (ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, event
return preventCooldown, nil
}
// IsAsync implements actor interface
func (ActorScript) IsAsync() bool { return false }
func (ActorScript) Name() string { return "script" }
// Name implements actor interface
func (ActorScript) Name() string { return "script" }
// Validate implements actor interface
func (ActorScript) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
cmd, err := attrs.StringSlice("command")
if err != nil || len(cmd) == 0 {

20
auth.go
View file

@ -9,7 +9,7 @@ import (
"github.com/gofrs/uuid/v3"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
"github.com/Luzifer/twitch-bot/v3/plugins"
@ -39,7 +39,7 @@ func init() {
},
} {
if err := registerRoute(rd); err != nil {
log.WithError(err).Fatal("Unable to register auth routes")
logrus.WithError(err).Fatal("Unable to register auth routes")
}
}
}
@ -71,7 +71,11 @@ func handleAuthUpdateBotToken(w http.ResponseWriter, r *http.Request) {
http.Error(w, errors.Wrap(err, "getting access token").Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
defer func() {
if err := resp.Body.Close(); err != nil {
logrus.WithError(err).Error("closing response body (leaked fd)")
}
}()
var rData twitch.OAuthTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&rData); err != nil {
@ -79,7 +83,7 @@ func handleAuthUpdateBotToken(w http.ResponseWriter, r *http.Request) {
return
}
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser()
_, botUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser(r.Context())
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return
@ -129,7 +133,11 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
http.Error(w, errors.Wrap(err, "getting access token").Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
defer func() {
if err := resp.Body.Close(); err != nil {
logrus.WithError(err).Error("closing response body (leaked fd)")
}
}()
var rData twitch.OAuthTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&rData); err != nil {
@ -137,7 +145,7 @@ func handleAuthUpdateChannelGrant(w http.ResponseWriter, r *http.Request) {
return
}
_, grantUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser()
_, grantUser, err := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, rData.AccessToken, "").GetAuthorizedUser(r.Context())
if err != nil {
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
return

View file

@ -31,7 +31,7 @@ func authBackendTwitchToken(token string) (modules []string, expiresAt time.Time
var httpError twitch.HTTPError
id, user, err := tc.GetAuthorizedUser()
id, user, err := tc.GetAuthorizedUser(context.Background())
switch {
case err == nil:
// We got a valid user, continue check below

View file

@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"strings"
"sync"
@ -71,7 +72,7 @@ func (a *autoMessage) CanSend() bool {
}
if a.OnlyOnLive {
streamLive, err := twitchClient.HasLiveStream(strings.TrimLeft(a.Channel, "#"))
streamLive, err := twitchClient.HasLiveStream(context.Background(), strings.TrimLeft(a.Channel, "#"))
if err != nil {
log.WithError(err).Error("Unable to determine channel live status")
return false

View file

@ -16,6 +16,6 @@ func getAuthorizationFromRequest(r *http.Request) (string, *twitch.Client, error
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, token, "")
_, user, err := tc.GetAuthorizedUser()
_, user, err := tc.GetAuthorizedUser(r.Context())
return user, tc, errors.Wrap(err, "getting authorized user")
}

View file

@ -13,7 +13,7 @@ import (
"github.com/gofrs/uuid/v3"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
"gopkg.in/irc.v4"
@ -23,7 +23,11 @@ import (
"github.com/Luzifer/twitch-bot/v3/plugins"
)
const expectedMinConfigVersion = 2
const (
expectedMinConfigVersion = 2
rawLogDirPerm = 0o755
rawLogFilePerm = 0o644
)
var (
//go:embed default_config.yaml
@ -121,10 +125,10 @@ func loadConfig(filename string) error {
if err = config.CloseRawMessageWriter(); err != nil {
return errors.Wrap(err, "closing old raw log writer")
}
if err = os.MkdirAll(path.Dir(tmpConfig.RawLog), 0o755); err != nil { //nolint:gomnd // This is a common directory permission
if err = os.MkdirAll(path.Dir(tmpConfig.RawLog), rawLogDirPerm); err != nil {
return errors.Wrap(err, "creating directories for raw log")
}
if tmpConfig.rawLogWriter, err = os.OpenFile(tmpConfig.RawLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644); err != nil { //nolint:gomnd // This is a common file permission
if tmpConfig.rawLogWriter, err = os.OpenFile(tmpConfig.RawLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, rawLogFilePerm); err != nil {
return errors.Wrap(err, "opening raw log for appending")
}
}
@ -132,7 +136,7 @@ func loadConfig(filename string) error {
config = tmpConfig
timerService.UpdatePermitTimeout(tmpConfig.PermitTimeout)
log.WithFields(log.Fields{
logrus.WithFields(logrus.Fields{
"auto_messages": len(config.AutoMessages),
"rules": len(config.Rules),
"channels": len(config.Channels),
@ -145,11 +149,15 @@ func loadConfig(filename string) error {
}
func parseConfigFromYAML(filename string, obj interface{}, strict bool) error {
f, err := os.Open(filename)
f, err := os.Open(filename) //#nosec:G304 // This is intended to open a variable file
if err != nil {
return errors.Wrap(err, "open config file")
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
logrus.WithError(err).Error("closing config file (leaked fd)")
}
}()
decoder := yaml.NewDecoder(f)
decoder.KnownFields(strict)
@ -205,10 +213,13 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
fmt.Fprintf(tmpFile, "# Automatically updated by %s using Config-Editor frontend, last update: %s\n", authorName, time.Now().Format(time.RFC3339))
if err = yaml.NewEncoder(tmpFile).Encode(obj); err != nil {
tmpFile.Close()
tmpFile.Close() //nolint:errcheck,gosec,revive
return errors.Wrap(err, "encoding config")
}
tmpFile.Close()
if err = tmpFile.Close(); err != nil {
return fmt.Errorf("closing temp config: %w", err)
}
if err = os.Rename(tmpFileName, filename); err != nil {
return errors.Wrap(err, "moving config to location")
@ -220,7 +231,7 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
git := newGitHelper(path.Dir(filename))
if !git.HasRepo() {
log.Error("Instructed to track changes using Git, but config not in repo")
logrus.Error("Instructed to track changes using Git, but config not in repo")
return nil
}
@ -231,11 +242,15 @@ func writeConfigToYAML(filename, authorName, authorEmail, summary string, obj *c
}
func writeDefaultConfigFile(filename string) error {
f, err := os.Create(filename)
f, err := os.Create(filename) //#nosec:G304 // This is intended to open a variable file
if err != nil {
return errors.Wrap(err, "creating config file")
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
logrus.WithError(err).Error("closing config file (leaked fd)")
}
}()
_, err = f.Write(defaultConfigurationYAML)
return errors.Wrap(err, "writing default config")
@ -276,11 +291,16 @@ func (c configAuthToken) validate(token string) error {
}
}
func (c *configFile) CloseRawMessageWriter() error {
func (c *configFile) CloseRawMessageWriter() (err error) {
if c == nil || c.rawLogWriter == nil {
return nil
}
return c.rawLogWriter.Close()
if err = c.rawLogWriter.Close(); err != nil {
return fmt.Errorf("closing raw-log writer: %w", err)
}
return nil
}
func (c configFile) GetMatchingRules(m *irc.Message, event *string, eventData *plugins.FieldCollection) []*plugins.Rule {
@ -319,14 +339,14 @@ func (configFile) fixedDuration(d time.Duration) time.Duration {
if d > time.Second {
return d
}
return d * time.Second
return d * time.Second //nolint:durationcheck // Error is handled before
}
func (configFile) fixedDurationPtr(d *time.Duration) *time.Duration {
if d == nil || *d >= time.Second {
return d
}
fd := *d * time.Second
fd := *d * time.Second //nolint:durationcheck // Error is handled before
return &fd
}
@ -368,11 +388,11 @@ func (c *configFile) fixTokenHashStorage() (err error) {
func (c *configFile) runLoadChecks() (err error) {
if len(c.Channels) == 0 {
log.Warn("Loaded config with empty channel list")
logrus.Warn("Loaded config with empty channel list")
}
if len(c.Rules) == 0 {
log.Warn("Loaded config with empty ruleset")
logrus.Warn("Loaded config with empty ruleset")
}
var seen []string
@ -397,7 +417,7 @@ func (c *configFile) updateAutoMessagesFromConfig(old *configFile) {
nam.lastMessageSent = time.Now()
if !nam.IsValid() {
log.WithField("index", idx).Warn("Auto-Message configuration is invalid and therefore disabled")
logrus.WithField("index", idx).Warn("Auto-Message configuration is invalid and therefore disabled")
}
if old == nil {
@ -426,7 +446,7 @@ func (c configFile) validateRuleActions() error {
var hasError bool
for _, r := range c.Rules {
logger := log.WithField("rule", r.MatcherID())
logger := logrus.WithField("rule", r.MatcherID())
if err := r.Validate(validateTemplate); err != nil {
logger.WithError(err).Error("Rule reported invalid config")

View file

@ -53,7 +53,7 @@ func registerEditorFrontend() {
return
}
io.Copy(w, f)
io.Copy(w, f) //nolint:errcheck,gosec
})
router.HandleFunc("/editor/vars.json", func(w http.ResponseWriter, r *http.Request) {

View file

@ -244,7 +244,7 @@ func configEditorHandleGeneralUpdate(w http.ResponseWriter, r *http.Request) {
}
for i := range payload.BotEditors {
usr, err := twitchClient.GetUserInformation(payload.BotEditors[i])
usr, err := twitchClient.GetUserInformation(r.Context(), payload.BotEditors[i])
if err != nil {
http.Error(w, errors.Wrap(err, "getting bot editor profile").Error(), http.StatusInternalServerError)
return

View file

@ -143,7 +143,7 @@ func configEditorGlobalGetModules(w http.ResponseWriter, _ *http.Request) {
}
func configEditorGlobalGetUser(w http.ResponseWriter, r *http.Request) {
usr, err := twitchClient.GetUserInformation(r.FormValue("user"))
usr, err := twitchClient.GetUserInformation(r.Context(), r.FormValue("user"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -160,7 +160,7 @@ func configEditorGlobalSubscribe(w http.ResponseWriter, r *http.Request) {
log.WithError(err).Error("Unable to initialize websocket")
return
}
defer conn.Close()
defer conn.Close() //nolint:errcheck
var (
frontendNotify = make(chan string, 1)
@ -190,7 +190,6 @@ func configEditorGlobalSubscribe(w http.ResponseWriter, r *http.Request) {
log.WithError(err).Debug("Unable to send websocket ping")
return
}
}
}
}

View file

@ -90,7 +90,7 @@ func configEditorRulesAdd(w http.ResponseWriter, r *http.Request) {
}
if msg.SubscribeFrom != nil {
if _, err = msg.UpdateFromSubscription(); err != nil {
if _, err = msg.UpdateFromSubscription(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View file

@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"math/rand"
@ -24,7 +25,7 @@ func updateConfigFromRemote() {
for _, r := range cfg.Rules {
logger := log.WithField("rule", r.MatcherID())
rhu, err := r.UpdateFromSubscription()
rhu, err := r.UpdateFromSubscription(context.Background())
if err != nil {
logger.WithError(err).Error("updating rule")
continue

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/Masterminds/sprig/v3"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"gopkg.in/irc.v4"
"github.com/Luzifer/go_helpers/v2/str"
@ -78,7 +78,7 @@ func (t *templateFuncProvider) Register(name string, fg plugins.TemplateFuncGett
defer t.lock.Unlock()
if _, ok := t.funcs[name]; ok {
log.Fatalf("Duplicate registration of %q template function", name) //nolint:gocritic // Yeah, the unlock will not run but the process will end
logrus.Fatalf("Duplicate registration of %q template function", name)
}
t.funcs[name] = fg
@ -108,7 +108,7 @@ func init() {
var parts []string
for idx, div := range []time.Duration{time.Hour, time.Minute, time.Second} {
part := dLeft / div
dLeft -= part * div
dLeft -= part * div //nolint:durationcheck // One is static, this is fine
if len(units) <= idx || units[idx] == "" {
continue

2
git.go
View file

@ -56,6 +56,6 @@ func (g gitHelper) HasRepo() bool {
return err == nil
}
func (g gitHelper) getSignature(name, mail string) *object.Signature {
func (gitHelper) getSignature(name, mail string) *object.Signature {
return &object.Signature{Name: name, Email: mail, When: time.Now()}
}

View file

@ -1,6 +1,9 @@
// Package announce contains a chat essage handler to create
// announcements from the bot
package announce
import (
"context"
"regexp"
"github.com/pkg/errors"
@ -16,6 +19,7 @@ var (
announceChatcommandRegex = regexp.MustCompile(`^/announce(|blue|green|orange|purple) +(.+)$`)
)
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
botTwitchClient = args.GetTwitchClient()
@ -32,7 +36,7 @@ func handleChatCommand(m *irc.Message) error {
return errors.New("announce message does not match required format")
}
if err := botTwitchClient.SendChatAnnouncement(channel, matches[1], matches[2]); err != nil {
if err := botTwitchClient.SendChatAnnouncement(context.Background(), channel, matches[1], matches[2]); err != nil {
return errors.Wrap(err, "sending announcement")
}

View file

@ -1,6 +1,9 @@
// Package ban contains actors to ban/unban users in a channel
package ban
import (
"context"
"fmt"
"net/http"
"regexp"
@ -21,7 +24,8 @@ var (
banChatcommandRegex = regexp.MustCompile(`^/ban +([^\s]+) +(.+)$`)
)
func Register(args plugins.RegistrationArguments) error {
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) (err error) {
botTwitchClient = args.GetTwitchClient()
formatMessage = args.FormatMessage
@ -45,7 +49,7 @@ func Register(args plugins.RegistrationArguments) error {
},
})
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
Description: "Executes a ban of an user in the specified channel",
HandlerFunc: handleAPIBan,
Method: http.MethodPost,
@ -72,7 +76,9 @@ func Register(args plugins.RegistrationArguments) error {
Name: "user",
},
},
})
}); err != nil {
return fmt.Errorf("registering API route: %w", err)
}
args.RegisterMessageModFunc("/ban", handleChatCommand)
@ -81,7 +87,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{}
func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
ptrStringEmpty := func(v string) *string { return &v }("")
reason, err := formatMessage(attrs.MustString("reason", ptrStringEmpty), m, r, eventData)
@ -91,6 +97,7 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
return false, errors.Wrap(
botTwitchClient.BanUser(
context.Background(),
plugins.DeriveChannel(m, eventData),
plugins.DeriveUser(m, eventData),
0,
@ -100,10 +107,10 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData
)
}
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (actor) IsAsync() bool { return false }
func (actor) Name() string { return actorName }
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
reasonTemplate, err := attrs.String("reason")
if err != nil || reasonTemplate == "" {
return errors.New("reason must be non-empty string")
@ -124,7 +131,7 @@ func handleAPIBan(w http.ResponseWriter, r *http.Request) {
reason = r.FormValue("reason")
)
if err := botTwitchClient.BanUser(channel, user, 0, reason); err != nil {
if err := botTwitchClient.BanUser(r.Context(), channel, user, 0, reason); err != nil {
http.Error(w, errors.Wrap(err, "issuing ban").Error(), http.StatusInternalServerError)
return
}
@ -140,7 +147,7 @@ func handleChatCommand(m *irc.Message) error {
return errors.New("ban message does not match required format")
}
if err := botTwitchClient.BanUser(channel, matches[1], 0, matches[2]); err != nil {
if err := botTwitchClient.BanUser(context.Background(), channel, matches[1], 0, matches[2]); err != nil {
return errors.Wrap(err, "executing ban")
}

View file

@ -1,3 +1,5 @@
// Package clip contains an actor to create clips on behalf of a
// channels owner
package clip
import (
@ -22,6 +24,7 @@ var (
ptrStringEmpty = func(s string) *string { return &s }("")
)
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
formatMessage = args.FormatMessage
hasPerm = args.HasPermissionForChannel

View file

@ -1,3 +1,5 @@
// Package clipdetector contains an actor to detect clip links in a
// message and populate a template variable
package clipdetector
import (
@ -19,6 +21,7 @@ var (
clipIDScanner = regexp.MustCompile(`(?:clips\.twitch\.tv|www\.twitch\.tv/[^/]*/clip)/([A-Za-z0-9_-]+)`)
)
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
botTwitchClient = args.GetTwitchClient()
@ -33,8 +36,10 @@ func Register(args plugins.RegistrationArguments) error {
return nil
}
// Actor implements the actor interface
type Actor struct{}
// Execute implements the actor interface
func (Actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
if eventData.HasAll("clips") {
// We already detected clips, lets not do it again
@ -70,8 +75,11 @@ func (Actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
return false, nil
}
// IsAsync implements the actor interface
func (Actor) IsAsync() bool { return false }
// Name implements the actor interface
func (Actor) Name() string { return actorName }
// Validate implements the actor interface
func (Actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) error { return nil }

View file

@ -1,3 +1,4 @@
// Package commercial contains an actor to run commercials in a channel
package commercial
import (
@ -27,6 +28,7 @@ var (
commercialChatcommandRegex = regexp.MustCompile(`^/commercial ([0-9]+)$`)
)
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
formatMessage = args.FormatMessage
permCheckFn = args.HasPermissionForChannel
@ -70,10 +72,10 @@ func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *
return false, startCommercial(strings.TrimLeft(plugins.DeriveChannel(m, eventData), "#"), durationStr)
}
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (actor) IsAsync() bool { return false }
func (actor) Name() string { return actorName }
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
durationTemplate, err := attrs.String("duration")
if err != nil || durationTemplate == "" {
return errors.New("duration must be non-empty string")

View file

@ -1,3 +1,5 @@
// Package counter contains actors and template functions to work with
// database stored counters
package counter
import (
@ -22,20 +24,22 @@ var (
ptrStringEmpty = func(s string) *string { return &s }("")
)
// Register provides the plugins.RegisterFunc
//
//nolint:funlen // This function is a few lines too long but only contains definitions
func Register(args plugins.RegistrationArguments) error {
func Register(args plugins.RegistrationArguments) (err error) {
db = args.GetDatabaseConnector()
if err := db.DB().AutoMigrate(&Counter{}); err != nil {
if err = db.DB().AutoMigrate(&counter{}); err != nil {
return errors.Wrap(err, "applying schema migration")
}
args.RegisterCopyDatabaseFunc("counter", func(src, target *gorm.DB) error {
return database.CopyObjects(src, target, &Counter{})
return database.CopyObjects(src, target, &counter{}) //nolint:wrapcheck // internal helper
})
formatMessage = args.FormatMessage
args.RegisterActor("counter", func() plugins.Actor { return &ActorCounter{} })
args.RegisterActor("counter", func() plugins.Actor { return &actorCounter{} })
args.RegisterActorDocumentation(plugins.ActionDocumentation{
Description: "Update counter values",
@ -73,7 +77,7 @@ func Register(args plugins.RegistrationArguments) error {
},
})
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
Description: "Returns the (formatted) value as a plain string",
HandlerFunc: routeActorCounterGetValue,
Method: http.MethodGet,
@ -95,9 +99,11 @@ func Register(args plugins.RegistrationArguments) error {
Name: "name",
},
},
})
}); err != nil {
return fmt.Errorf("registering API route: %w", err)
}
args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
Description: "Updates the value of the counter",
HandlerFunc: routeActorCounterSetValue,
Method: http.MethodPatch,
@ -125,7 +131,9 @@ func Register(args plugins.RegistrationArguments) error {
Name: "name",
},
},
})
}); err != nil {
return fmt.Errorf("registering API route: %w", err)
}
args.RegisterTemplateFunction("channelCounter", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(name string) (string, error) {
@ -157,7 +165,7 @@ func Register(args plugins.RegistrationArguments) error {
},
})
args.RegisterTemplateFunction("counterTopList", plugins.GenericTemplateFunctionGetter(func(prefix string, n int) ([]Counter, error) {
args.RegisterTemplateFunction("counterTopList", plugins.GenericTemplateFunctionGetter(func(prefix string, n int) ([]counter, error) {
return getCounterTopList(db, prefix, n)
}), plugins.TemplateFuncDocumentation{
Description: "Returns the top n counters for the given prefix as objects with Name and Value fields",
@ -169,7 +177,7 @@ func Register(args plugins.RegistrationArguments) error {
})
args.RegisterTemplateFunction("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) (int64, error) {
return GetCounterValue(db, name)
return getCounterValue(db, name)
}), plugins.TemplateFuncDocumentation{
Description: "Returns the current value of the counter which identifier was supplied",
Syntax: "counterValue <counter name>",
@ -185,11 +193,11 @@ func Register(args plugins.RegistrationArguments) error {
mod = val[0]
}
if err := UpdateCounter(db, name, mod, false); err != nil {
if err := updateCounter(db, name, mod, false); err != nil {
return 0, errors.Wrap(err, "updating counter")
}
return GetCounterValue(db, name)
return getCounterValue(db, name)
}), plugins.TemplateFuncDocumentation{
Description: "Adds the given value (or 1 if no value) to the counter and returns its new value",
Syntax: "counterValueAdd <counter name> [increase=1]",
@ -202,9 +210,9 @@ func Register(args plugins.RegistrationArguments) error {
return nil
}
type ActorCounter struct{}
type actorCounter struct{}
func (a ActorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
func (actorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
counterName, err := formatMessage(attrs.MustString("counter", nil), m, r, eventData)
if err != nil {
return false, errors.Wrap(err, "preparing response")
@ -222,7 +230,7 @@ func (a ActorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, ev
}
return false, errors.Wrap(
UpdateCounter(db, counterName, counterValue, true),
updateCounter(db, counterName, counterValue, true),
"set counter",
)
}
@ -241,15 +249,15 @@ func (a ActorCounter) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, ev
}
return false, errors.Wrap(
UpdateCounter(db, counterName, counterStep, false),
updateCounter(db, counterName, counterStep, false),
"update counter",
)
}
func (a ActorCounter) IsAsync() bool { return false }
func (a ActorCounter) Name() string { return "counter" }
func (actorCounter) IsAsync() bool { return false }
func (actorCounter) Name() string { return "counter" }
func (a ActorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
func (actorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if cn, err := attrs.String("counter"); err != nil || cn == "" {
return errors.New("counter name must be non-empty string")
}
@ -269,7 +277,7 @@ func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
template = "%d"
}
cv, err := GetCounterValue(db, mux.Vars(r)["name"])
cv, err := getCounterValue(db, mux.Vars(r)["name"])
if err != nil {
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
return
@ -291,7 +299,7 @@ func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {
return
}
if err = UpdateCounter(db, mux.Vars(r)["name"], value, absolute); err != nil {
if err = updateCounter(db, mux.Vars(r)["name"], value, absolute); err != nil {
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
return
}

View file

@ -10,14 +10,14 @@ import (
)
type (
Counter struct {
counter struct {
Name string `gorm:"primaryKey"`
Value int64
}
)
func GetCounterValue(db database.Connector, counterName string) (int64, error) {
var c Counter
func getCounterValue(db database.Connector, counterName string) (int64, error) {
var c counter
err := helpers.Retry(func() error {
err := db.DB().First(&c, "name = ?", counterName).Error
@ -31,9 +31,10 @@ func GetCounterValue(db database.Connector, counterName string) (int64, error) {
return c.Value, errors.Wrap(err, "querying counter")
}
func UpdateCounter(db database.Connector, counterName string, value int64, absolute bool) error {
//revive:disable-next-line:flag-parameter
func updateCounter(db database.Connector, counterName string, value int64, absolute bool) error {
if !absolute {
cv, err := GetCounterValue(db, counterName)
cv, err := getCounterValue(db, counterName)
if err != nil {
return errors.Wrap(err, "getting previous value")
}
@ -46,14 +47,14 @@ func UpdateCounter(db database.Connector, counterName string, value int64, absol
return tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoUpdates: clause.AssignmentColumns([]string{"value"}),
}).Create(Counter{Name: counterName, Value: value}).Error
}).Create(counter{Name: counterName, Value: value}).Error
}),
"storing counter value",
)
}
func getCounterRank(db database.Connector, prefix, name string) (rank, count int64, err error) {
var cc []Counter
var cc []counter
if err = helpers.Retry(func() error {
return db.DB().
@ -74,8 +75,8 @@ func getCounterRank(db database.Connector, prefix, name string) (rank, count int
return rank, count, nil
}
func getCounterTopList(db database.Connector, prefix string, n int) ([]Counter, error) {
var cc []Counter
func getCounterTopList(db database.Connector, prefix string, n int) ([]counter, error) {
var cc []counter
err := helpers.Retry(func() error {
return db.DB().

View file

@ -12,34 +12,34 @@ import (
func TestCounterStoreLoop(t *testing.T) {
dbc := database.GetTestDatabase(t)
dbc.DB().AutoMigrate(&Counter{})
require.NoError(t, dbc.DB().AutoMigrate(&counter{}))
counterName := "mytestcounter"
v, err := GetCounterValue(dbc, counterName)
v, err := getCounterValue(dbc, counterName)
assert.NoError(t, err, "reading non-existent counter")
assert.Equal(t, int64(0), v, "expecting 0 counter value on non-existent counter")
err = UpdateCounter(dbc, counterName, 5, true)
err = updateCounter(dbc, counterName, 5, true)
assert.NoError(t, err, "inserting counter")
err = UpdateCounter(dbc, counterName, 1, false)
err = updateCounter(dbc, counterName, 1, false)
assert.NoError(t, err, "updating counter")
v, err = GetCounterValue(dbc, counterName)
v, err = getCounterValue(dbc, counterName)
assert.NoError(t, err, "reading existent counter")
assert.Equal(t, int64(6), v, "expecting counter value on existing counter")
}
func TestCounterTopListAndRank(t *testing.T) {
dbc := database.GetTestDatabase(t)
dbc.DB().AutoMigrate(&Counter{})
require.NoError(t, dbc.DB().AutoMigrate(&counter{}))
counterTemplate := `#example:test:%v`
for i := 0; i < 6; i++ {
require.NoError(
t,
UpdateCounter(dbc, fmt.Sprintf(counterTemplate, i), int64(i), true),
updateCounter(dbc, fmt.Sprintf(counterTemplate, i), int64(i), true),
"inserting counter %d", i,
)
}
@ -48,7 +48,7 @@ func TestCounterTopListAndRank(t *testing.T) {
require.NoError(t, err)
assert.Len(t, cc, 3)
assert.Equal(t, []Counter{
assert.Equal(t, []counter{
{Name: "#example:test:5", Value: 5},
{Name: "#example:test:4", Value: 4},
{Name: "#example:test:3", Value: 3},

View file

@ -1,3 +1,4 @@
// Package delay contains an actor to delay rule execution
package delay
import (
@ -11,6 +12,7 @@ import (
const actorName = "delay"
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
args.RegisterActor(actorName, func() plugins.Actor { return &actor{} })
@ -46,7 +48,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{}
func (a actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
func (actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
var (
ptrZeroDuration = func(v time.Duration) *time.Duration { return &v }(0)
delay = attrs.MustDuration("delay", ptrZeroDuration)
@ -66,9 +68,9 @@ func (a actor) Execute(_ *irc.Client, _ *irc.Message, _ *plugins.Rule, _ *plugin
return false, nil
}
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (actor) IsAsync() bool { return false }
func (actor) Name() string { return actorName }
func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
func (actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
return nil
}

View file

@ -1,6 +1,9 @@
// Package deleteactor contains an actor to delete messages
package deleteactor
import (
"context"
"github.com/pkg/errors"
"gopkg.in/irc.v4"
@ -12,6 +15,7 @@ const actorName = "delete"
var botTwitchClient *twitch.Client
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
botTwitchClient = args.GetTwitchClient()
@ -28,7 +32,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{}
func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, _ *plugins.FieldCollection) (preventCooldown bool, err error) {
func (actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData *plugins.FieldCollection, _ *plugins.FieldCollection) (preventCooldown bool, err error) {
msgID, ok := m.Tags["id"]
if !ok || msgID == "" {
return false, nil
@ -36,6 +40,7 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData
return false, errors.Wrap(
botTwitchClient.DeleteMessage(
context.Background(),
plugins.DeriveChannel(m, eventData),
msgID,
),
@ -43,9 +48,9 @@ func (a actor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, eventData
)
}
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (actor) IsAsync() bool { return false }
func (actor) Name() string { return actorName }
func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
func (actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
return nil
}

View file

@ -1,3 +1,5 @@
// Package eventmod contains an actor to modify event data during rule