mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-30 00:21:16 +00:00
Add code linting / binary publishing (#3)
This commit is contained in:
parent
8b575f7771
commit
0ae34112b6
11 changed files with 426 additions and 100 deletions
59
.golangci.yml
Normal file
59
.golangci.yml
Normal file
|
@ -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]
|
||||||
|
|
||||||
|
...
|
13
.repo-runner.yaml
Normal file
13
.repo-runner.yaml
Normal file
|
@ -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
|
14
Makefile
14
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:
|
pull_wiki:
|
||||||
git subtree pull --prefix=wiki https://github.com/Luzifer/twitch-bot.wiki.git master --squash
|
git subtree pull --prefix=wiki https://github.com/Luzifer/twitch-bot.wiki.git master --squash
|
||||||
|
|
|
@ -45,7 +45,7 @@ func init() {
|
||||||
return errors.Wrap(err, "encoding script input")
|
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.Env = os.Environ()
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Stdin = stdin
|
cmd.Stdin = stdin
|
||||||
|
|
5
irc.go
5
irc.go
|
@ -19,7 +19,6 @@ const (
|
||||||
badgeFounder = "founder"
|
badgeFounder = "founder"
|
||||||
badgeModerator = "moderator"
|
badgeModerator = "moderator"
|
||||||
badgeSubscriber = "subscriber"
|
badgeSubscriber = "subscriber"
|
||||||
badgeVIP = "vip"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ircHandler struct {
|
type ircHandler struct {
|
||||||
|
@ -135,7 +134,7 @@ func (i ircHandler) handlePermit(m *irc.Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
msgParts := strings.Split(m.Trailing(), " ")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +229,7 @@ func (ircHandler) ParseBadgeLevels(m *irc.Message) badgeCollection {
|
||||||
badges := strings.Split(badgeString, ",")
|
badges := strings.Split(badgeString, ",")
|
||||||
for _, b := range badges {
|
for _, b := range badges {
|
||||||
badgeParts := strings.Split(b, "/")
|
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")
|
log.WithField("badge", b).Warn("Malformed badge found")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
13
main.go
13
main.go
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -12,6 +13,8 @@ import (
|
||||||
"github.com/Luzifer/rconfig/v2"
|
"github.com/Luzifer/rconfig/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ircReconnectDelay = 100 * time.Millisecond
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg = struct {
|
cfg = struct {
|
||||||
CommandTimeout time.Duration `flag:"command-timeout" default:"30s" description:"Timeout for command execution"`
|
CommandTimeout time.Duration `flag:"command-timeout" default:"30s" description:"Timeout for command execution"`
|
||||||
|
@ -32,6 +35,13 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
for _, a := range os.Args {
|
||||||
|
if strings.HasPrefix(a, "-test.") {
|
||||||
|
// Skip initialize for test run
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rconfig.AutoEnv(true)
|
rconfig.AutoEnv(true)
|
||||||
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
||||||
log.Fatalf("Unable to parse commandline options: %s", err)
|
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() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
@ -93,7 +104,7 @@ func main() {
|
||||||
if err := irc.Run(); err != nil {
|
if err := irc.Run(); err != nil {
|
||||||
log.WithError(err).Error("IRC run exited unexpectedly")
|
log.WithError(err).Error("IRC run exited unexpectedly")
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(ircReconnectDelay)
|
||||||
ircDisconnected <- struct{}{}
|
ircDisconnected <- struct{}{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ func formatMessage(tplString string, m *irc.Message, r *rule, fields map[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
fields["msg"] = m
|
fields["msg"] = m
|
||||||
fields["permitTimeout"] = int64(*&config.PermitTimeout / time.Second)
|
fields["permitTimeout"] = int64(config.PermitTimeout / time.Second)
|
||||||
fields["username"] = m.User
|
fields["username"] = m.User
|
||||||
|
|
||||||
if m.Command == "PRIVMSG" && len(m.Params) > 0 {
|
if m.Command == "PRIVMSG" && len(m.Params) > 0 {
|
||||||
|
|
246
rule.go
246
rule.go
|
@ -51,8 +51,6 @@ func (r rule) MatcherID() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rule) Matches(m *irc.Message, event *string) bool {
|
func (r *rule) Matches(m *irc.Message, event *string) bool {
|
||||||
var err error
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
badges = ircHandler{}.ParseBadgeLevels(m)
|
badges = ircHandler{}.ParseBadgeLevels(m)
|
||||||
logger = log.WithFields(log.Fields{
|
logger = log.WithFields(log.Fields{
|
||||||
|
@ -61,69 +59,28 @@ func (r *rule) Matches(m *irc.Message, event *string) bool {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check Channel match
|
for _, matcher := range []func(*log.Entry, *irc.Message, *string, badgeCollection) bool{
|
||||||
if len(r.MatchChannels) > 0 {
|
r.allowExecuteChannelWhitelist,
|
||||||
if len(m.Params) == 0 || !str.StringInSlice(m.Params[0], r.MatchChannels) {
|
r.allowExecuteUserWhitelist,
|
||||||
logger.Trace("Non-Match: Channel")
|
r.allowExecuteEventWhitelist,
|
||||||
|
r.allowExecuteMessageMatcherWhitelist,
|
||||||
|
r.allowExecuteMessageMatcherBlacklist,
|
||||||
|
r.allowExecuteBadgeBlacklist,
|
||||||
|
r.allowExecuteBadgeWhitelist,
|
||||||
|
r.allowExecuteDisableOnPermit,
|
||||||
|
r.allowExecuteCooldown,
|
||||||
|
r.allowExecuteDisableOnOffline,
|
||||||
|
} {
|
||||||
|
if !matcher(logger, m, event, badges) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(r.MatchUsers) > 0 {
|
// Nothing objected: Matches!
|
||||||
if !str.StringInSlice(strings.ToLower(m.User), r.MatchUsers) {
|
return true
|
||||||
logger.Trace("Non-Match: Users")
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Event match
|
func (r *rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||||
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
|
|
||||||
for _, b := range r.DisableOn {
|
for _, b := range r.DisableOn {
|
||||||
if badges.Has(b) {
|
if badges.Has(b) {
|
||||||
logger.Tracef("Non-Match: Disable-Badge %s", 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
|
return true
|
||||||
if len(r.EnableOn) > 0 {
|
}
|
||||||
var userHasEnableBadge bool
|
|
||||||
for _, b := range r.EnableOn {
|
func (r *rule) allowExecuteBadgeWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||||
if badges.Has(b) {
|
if len(r.EnableOn) == 0 {
|
||||||
userHasEnableBadge = true
|
// No match criteria set, does not speak against matching
|
||||||
}
|
return true
|
||||||
}
|
}
|
||||||
if !userHasEnableBadge {
|
|
||||||
logger.Trace("Non-Match: No enable-badges")
|
for _, b := range r.EnableOn {
|
||||||
return false
|
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) {
|
if r.DisableOnPermit && timerStore.HasPermit(m.Params[0], m.User) {
|
||||||
logger.Trace("Non-Match: Permit")
|
logger.Trace("Non-Match: Permit")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether rule is in cooldown
|
return true
|
||||||
if r.Cooldown != nil && timerStore.InCooldown(r.MatcherID(), *r.Cooldown) {
|
}
|
||||||
var userHasSkipBadge bool
|
|
||||||
for _, b := range r.SkipCooldownFor {
|
func (r *rule) allowExecuteEventWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||||
if badges.Has(b) {
|
if r.MatchEvent == nil {
|
||||||
userHasSkipBadge = true
|
// 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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.DisableOnOffline {
|
return true
|
||||||
streamLive, err := twitch.HasLiveStream(strings.TrimLeft(m.Params[0], "#"))
|
}
|
||||||
if err != nil {
|
|
||||||
logger.WithError(err).Error("Unable to determine live status")
|
func (r *rule) allowExecuteMessageMatcherWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||||
return false
|
if r.MatchMessage == nil {
|
||||||
}
|
// No match criteria set, does not speak against matching
|
||||||
if !streamLive {
|
return true
|
||||||
logger.Trace("Non-Match: Stream offline")
|
}
|
||||||
|
|
||||||
|
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
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
164
rule_test.go
Normal file
164
rule_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ type timerType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
timerTypePermit timerType = iota
|
timerTypePermit timerType = iota
|
||||||
timerTypeChatMessage
|
|
||||||
timerTypeCooldown
|
timerTypeCooldown
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,7 +25,6 @@ type timerEntry struct {
|
||||||
type timer struct {
|
type timer struct {
|
||||||
timers map[string]timerEntry
|
timers map[string]timerEntry
|
||||||
lock *sync.RWMutex
|
lock *sync.RWMutex
|
||||||
kind timerType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTimer() *timer {
|
func newTimer() *timer {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const timeDay = 24 * time.Hour
|
||||||
|
|
||||||
var twitch = newTwitchClient()
|
var twitch = newTwitchClient()
|
||||||
|
|
||||||
type twitchClient struct {
|
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
|
// 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
|
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
|
// 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
|
return payload.Data[0].ID, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue