mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-09 16:50:01 +00:00
Use more flexible Actor format to allow addition of new actors (#5)
This commit is contained in:
parent
0db778f841
commit
ede8a95ed4
17 changed files with 453 additions and 327 deletions
|
@ -8,8 +8,15 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.Ban == nil {
|
||||
registerAction(func() Actor { return &ActorBan{} })
|
||||
}
|
||||
|
||||
type ActorBan struct {
|
||||
Ban *string `json:"ban" yaml:"ban"`
|
||||
}
|
||||
|
||||
func (a ActorBan) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.Ban == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -18,10 +25,12 @@ func init() {
|
|||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
m.Params[0],
|
||||
fmt.Sprintf("/ban %s %s", m.User, *r.Ban),
|
||||
fmt.Sprintf("/ban %s %s", m.User, *a.Ban),
|
||||
},
|
||||
}),
|
||||
"sending timeout",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorBan) IsAsync() bool { return false }
|
||||
func (a ActorBan) Name() string { return "ban" }
|
||||
|
|
|
@ -8,18 +8,27 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.Counter == nil {
|
||||
registerAction(func() Actor { return &ActorCounter{} })
|
||||
}
|
||||
|
||||
type ActorCounter struct {
|
||||
CounterSet *string `json:"counter_set" yaml:"counter_set"`
|
||||
CounterStep *int64 `json:"counter_step" yaml:"counter_step"`
|
||||
Counter *string `json:"counter" yaml:"counter"`
|
||||
}
|
||||
|
||||
func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.Counter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
counterName, err := formatMessage(*r.Counter, m, ruleDef, nil)
|
||||
counterName, err := formatMessage(*a.Counter, m, r, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "preparing response")
|
||||
}
|
||||
|
||||
if r.CounterSet != nil {
|
||||
parseValue, err := formatMessage(*r.CounterSet, m, ruleDef, nil)
|
||||
if a.CounterSet != nil {
|
||||
parseValue, err := formatMessage(*a.CounterSet, m, r, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "execute counter value template")
|
||||
}
|
||||
|
@ -36,13 +45,15 @@ func init() {
|
|||
}
|
||||
|
||||
var counterStep int64 = 1
|
||||
if r.CounterStep != nil {
|
||||
counterStep = *r.CounterStep
|
||||
if a.CounterStep != nil {
|
||||
counterStep = *a.CounterStep
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
store.UpdateCounter(counterName, counterStep, false),
|
||||
"update counter",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorCounter) IsAsync() bool { return false }
|
||||
func (a ActorCounter) Name() string { return "counter" }
|
||||
|
|
|
@ -8,17 +8,27 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.Delay == 0 && r.DelayJitter == 0 {
|
||||
registerAction(func() Actor { return &ActorDelay{} })
|
||||
}
|
||||
|
||||
type ActorDelay struct {
|
||||
Delay time.Duration `json:"delay" yaml:"delay"`
|
||||
DelayJitter time.Duration `json:"delay_jitter" yaml:"delay_jitter"`
|
||||
}
|
||||
|
||||
func (a ActorDelay) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.Delay == 0 && a.DelayJitter == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalDelay := r.Delay
|
||||
if r.DelayJitter > 0 {
|
||||
totalDelay += time.Duration(rand.Int63n(int64(r.DelayJitter))) // #nosec: G404 // It's just time, no need for crypto/rand
|
||||
totalDelay := a.Delay
|
||||
if a.DelayJitter > 0 {
|
||||
totalDelay += time.Duration(rand.Int63n(int64(a.DelayJitter))) // #nosec: G404 // It's just time, no need for crypto/rand
|
||||
}
|
||||
|
||||
time.Sleep(totalDelay)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorDelay) IsAsync() bool { return false }
|
||||
func (a ActorDelay) Name() string { return "delay" }
|
||||
|
|
|
@ -8,8 +8,15 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.DeleteMessage == nil || !*r.DeleteMessage {
|
||||
registerAction(func() Actor { return &ActorDelete{} })
|
||||
}
|
||||
|
||||
type ActorDelete struct {
|
||||
DeleteMessage *bool `json:"delete_message" yaml:"delete_message"`
|
||||
}
|
||||
|
||||
func (a ActorDelete) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.DeleteMessage == nil || !*a.DeleteMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -28,5 +35,7 @@ func init() {
|
|||
}),
|
||||
"sending delete",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorDelete) IsAsync() bool { return false }
|
||||
func (a ActorDelete) Name() string { return "delete" }
|
||||
|
|
|
@ -6,12 +6,19 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.RawMessage == nil {
|
||||
registerAction(func() Actor { return &ActorRaw{} })
|
||||
}
|
||||
|
||||
type ActorRaw struct {
|
||||
RawMessage *string `json:"raw_message" yaml:"raw_message"`
|
||||
}
|
||||
|
||||
func (a ActorRaw) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.RawMessage == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rawMsg, err := formatMessage(*r.RawMessage, m, ruleDef, nil)
|
||||
rawMsg, err := formatMessage(*a.RawMessage, m, r, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "preparing raw message")
|
||||
}
|
||||
|
@ -25,5 +32,7 @@ func init() {
|
|||
c.WriteMessage(msg),
|
||||
"sending raw message",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorRaw) IsAsync() bool { return false }
|
||||
func (a ActorRaw) Name() string { return "raw" }
|
||||
|
|
|
@ -6,17 +6,26 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.Respond == nil {
|
||||
registerAction(func() Actor { return &ActorRespond{} })
|
||||
}
|
||||
|
||||
type ActorRespond struct {
|
||||
Respond *string `json:"respond" yaml:"respond"`
|
||||
RespondAsReply *bool `json:"respond_as_reply" yaml:"respond_as_reply"`
|
||||
RespondFallback *string `json:"respond_fallback" yaml:"respond_fallback"`
|
||||
}
|
||||
|
||||
func (a ActorRespond) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.Respond == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
msg, err := formatMessage(*r.Respond, m, ruleDef, nil)
|
||||
msg, err := formatMessage(*a.Respond, m, r, nil)
|
||||
if err != nil {
|
||||
if r.RespondFallback == nil {
|
||||
if a.RespondFallback == nil {
|
||||
return errors.Wrap(err, "preparing response")
|
||||
}
|
||||
if msg, err = formatMessage(*r.RespondFallback, m, ruleDef, nil); err != nil {
|
||||
if msg, err = formatMessage(*a.RespondFallback, m, r, nil); err != nil {
|
||||
return errors.Wrap(err, "preparing response fallback")
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +38,7 @@ func init() {
|
|||
},
|
||||
}
|
||||
|
||||
if r.RespondAsReply != nil && *r.RespondAsReply {
|
||||
if a.RespondAsReply != nil && *a.RespondAsReply {
|
||||
id, ok := m.GetTag("id")
|
||||
if ok {
|
||||
if ircMessage.Tags == nil {
|
||||
|
@ -43,5 +52,7 @@ func init() {
|
|||
c.WriteMessage(ircMessage),
|
||||
"sending response",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorRespond) IsAsync() bool { return false }
|
||||
func (a ActorRespond) Name() string { return "respond" }
|
||||
|
|
|
@ -12,14 +12,21 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if len(r.Command) == 0 {
|
||||
registerAction(func() Actor { return &ActorScript{} })
|
||||
}
|
||||
|
||||
type ActorScript struct {
|
||||
Command []string `json:"command" yaml:"command"`
|
||||
}
|
||||
|
||||
func (a ActorScript) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if len(a.Command) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var command []string
|
||||
for _, arg := range r.Command {
|
||||
tmp, err := formatMessage(arg, m, ruleDef, nil)
|
||||
for _, arg := range a.Command {
|
||||
tmp, err := formatMessage(arg, m, r, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "execute command argument template")
|
||||
}
|
||||
|
@ -61,7 +68,7 @@ func init() {
|
|||
}
|
||||
|
||||
var (
|
||||
actions []*ruleAction
|
||||
actions []*RuleAction
|
||||
decoder = json.NewDecoder(stdout)
|
||||
)
|
||||
|
||||
|
@ -71,11 +78,13 @@ func init() {
|
|||
}
|
||||
|
||||
for _, action := range actions {
|
||||
if err := triggerActions(c, m, ruleDef, action); err != nil {
|
||||
if err := triggerActions(c, m, r, action); err != nil {
|
||||
return errors.Wrap(err, "execute returned action")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorScript) IsAsync() bool { return false }
|
||||
func (a ActorScript) Name() string { return "script" }
|
||||
|
|
|
@ -9,8 +9,15 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.Timeout == nil {
|
||||
registerAction(func() Actor { return &ActorTimeout{} })
|
||||
}
|
||||
|
||||
type ActorTimeout struct {
|
||||
Timeout *time.Duration `json:"timeout" yaml:"timeout"`
|
||||
}
|
||||
|
||||
func (a ActorTimeout) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.Timeout == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -19,10 +26,12 @@ func init() {
|
|||
Command: "PRIVMSG",
|
||||
Params: []string{
|
||||
m.Params[0],
|
||||
fmt.Sprintf("/timeout %s %d", m.User, *r.Timeout/time.Second),
|
||||
fmt.Sprintf("/timeout %s %d", m.User, *a.Timeout/time.Second),
|
||||
},
|
||||
}),
|
||||
"sending timeout",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorTimeout) IsAsync() bool { return false }
|
||||
func (a ActorTimeout) Name() string { return "timeout" }
|
||||
|
|
|
@ -8,17 +8,25 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
|
||||
if r.WhisperTo == nil || r.WhisperMessage == nil {
|
||||
registerAction(func() Actor { return &ActorWhisper{} })
|
||||
}
|
||||
|
||||
type ActorWhisper struct {
|
||||
WhisperMessage *string `json:"whisper_message" yaml:"whisper_message"`
|
||||
WhisperTo *string `json:"whisper_to" yaml:"whisper_to"`
|
||||
}
|
||||
|
||||
func (a ActorWhisper) Execute(c *irc.Client, m *irc.Message, r *Rule) error {
|
||||
if a.WhisperTo == nil || a.WhisperMessage == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
to, err := formatMessage(*r.WhisperTo, m, ruleDef, nil)
|
||||
to, err := formatMessage(*a.WhisperTo, m, r, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "preparing whisper receiver")
|
||||
}
|
||||
|
||||
msg, err := formatMessage(*r.WhisperMessage, m, ruleDef, nil)
|
||||
msg, err := formatMessage(*a.WhisperMessage, m, r, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "preparing whisper message")
|
||||
}
|
||||
|
@ -38,5 +46,7 @@ func init() {
|
|||
}),
|
||||
"sending whisper",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (a ActorWhisper) IsAsync() bool { return false }
|
||||
func (a ActorWhisper) Name() string { return "whisper" }
|
||||
|
|
48
actions.go
48
actions.go
|
@ -8,26 +8,58 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type (
|
||||
Actor interface {
|
||||
// Execute will be called after the config was read into the Actor
|
||||
Execute(*irc.Client, *irc.Message, *Rule) error
|
||||
// IsAsync may return true if the Execute function is to be executed
|
||||
// in a Go routine as of long runtime. Normally it should return false
|
||||
// except in very specific cases
|
||||
IsAsync() bool
|
||||
// Name must return an unique name for the actor in order to identify
|
||||
// it in the logs for debugging purposes
|
||||
Name() string
|
||||
}
|
||||
ActorCreationFunc func() Actor
|
||||
)
|
||||
|
||||
var (
|
||||
availableActions []actionFunc
|
||||
availableActions []ActorCreationFunc
|
||||
availableActionsLock = new(sync.RWMutex)
|
||||
)
|
||||
|
||||
type actionFunc func(*irc.Client, *irc.Message, *rule, *ruleAction) error
|
||||
|
||||
func registerAction(af actionFunc) {
|
||||
func registerAction(af ActorCreationFunc) {
|
||||
availableActionsLock.Lock()
|
||||
defer availableActionsLock.Unlock()
|
||||
|
||||
availableActions = append(availableActions, af)
|
||||
}
|
||||
|
||||
func triggerActions(c *irc.Client, m *irc.Message, rule *rule, ra *ruleAction) error {
|
||||
func triggerActions(c *irc.Client, m *irc.Message, rule *Rule, ra *RuleAction) error {
|
||||
availableActionsLock.RLock()
|
||||
defer availableActionsLock.RUnlock()
|
||||
|
||||
for _, af := range availableActions {
|
||||
if err := af(c, m, rule, ra); err != nil {
|
||||
for _, acf := range availableActions {
|
||||
var (
|
||||
a = acf()
|
||||
logger = log.WithField("actor", a.Name())
|
||||
)
|
||||
|
||||
if err := ra.Unmarshal(a); err != nil {
|
||||
logger.WithError(err).Trace("Unable to unmarshal config")
|
||||
continue
|
||||
}
|
||||
|
||||
if a.IsAsync() {
|
||||
go func() {
|
||||
if err := a.Execute(c, m, rule); err != nil {
|
||||
logger.WithError(err).Error("Error in async actor")
|
||||
}
|
||||
}()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := a.Execute(c, m, rule); err != nil {
|
||||
return errors.Wrap(err, "execute action")
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +76,6 @@ func handleMessage(c *irc.Client, m *irc.Message, event *string) {
|
|||
}
|
||||
|
||||
// Lock command
|
||||
r.SetCooldown(m)
|
||||
r.setCooldown(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ type configFile struct {
|
|||
PermitAllowModerator bool `yaml:"permit_allow_moderator"`
|
||||
PermitTimeout time.Duration `yaml:"permit_timeout"`
|
||||
RawLog string `yaml:"raw_log"`
|
||||
Rules []*rule `yaml:"rules"`
|
||||
Rules []*Rule `yaml:"rules"`
|
||||
Variables map[string]interface{} `yaml:"variables"`
|
||||
|
||||
rawLogWriter io.WriteCloser
|
||||
|
@ -125,14 +125,14 @@ func (c *configFile) CloseRawMessageWriter() error {
|
|||
return c.rawLogWriter.Close()
|
||||
}
|
||||
|
||||
func (c configFile) GetMatchingRules(m *irc.Message, event *string) []*rule {
|
||||
func (c configFile) GetMatchingRules(m *irc.Message, event *string) []*Rule {
|
||||
configLock.RLock()
|
||||
defer configLock.RUnlock()
|
||||
|
||||
var out []*rule
|
||||
var out []*Rule
|
||||
|
||||
for _, r := range c.Rules {
|
||||
if r.Matches(m, event) {
|
||||
if r.matches(m, event) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
var tplFuncs = newTemplateFuncProvider()
|
||||
|
||||
type (
|
||||
templateFuncGetter func(*irc.Message, *rule, map[string]interface{}) interface{}
|
||||
templateFuncGetter func(*irc.Message, *Rule, map[string]interface{}) interface{}
|
||||
templateFuncProvider struct {
|
||||
funcs map[string]templateFuncGetter
|
||||
lock *sync.RWMutex
|
||||
|
@ -28,7 +28,7 @@ func newTemplateFuncProvider() *templateFuncProvider {
|
|||
return out
|
||||
}
|
||||
|
||||
func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *rule, fields map[string]interface{}) template.FuncMap {
|
||||
func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *Rule, fields map[string]interface{}) template.FuncMap {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
|
@ -49,7 +49,7 @@ func (t *templateFuncProvider) Register(name string, fg templateFuncGetter) {
|
|||
}
|
||||
|
||||
func genericTemplateFunctionGetter(f interface{}) templateFuncGetter {
|
||||
return func(*irc.Message, *rule, map[string]interface{}) interface{} { return f }
|
||||
return func(*irc.Message, *Rule, map[string]interface{}) interface{} { return f }
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
tplFuncs.Register("channelCounter", func(m *irc.Message, r *rule, fields map[string]interface{}) interface{} {
|
||||
tplFuncs.Register("channelCounter", func(m *irc.Message, r *Rule, fields map[string]interface{}) interface{} {
|
||||
return func(name string) (string, error) {
|
||||
channel, ok := fields["channel"].(string)
|
||||
if !ok {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
tplFuncs.Register("arg", func(m *irc.Message, r *rule, fields map[string]interface{}) interface{} {
|
||||
tplFuncs.Register("arg", func(m *irc.Message, r *Rule, fields map[string]interface{}) interface{} {
|
||||
return func(arg int) (string, error) {
|
||||
msgParts := strings.Split(m.Trailing(), " ")
|
||||
if len(msgParts) <= arg {
|
||||
|
@ -21,7 +21,7 @@ func init() {
|
|||
|
||||
tplFuncs.Register("fixUsername", genericTemplateFunctionGetter(func(username string) string { return strings.TrimLeft(username, "@#") }))
|
||||
|
||||
tplFuncs.Register("group", func(m *irc.Message, r *rule, fields map[string]interface{}) interface{} {
|
||||
tplFuncs.Register("group", func(m *irc.Message, r *Rule, fields map[string]interface{}) interface{} {
|
||||
return func(idx int) (string, error) {
|
||||
fields := r.matchMessage.FindStringSubmatch(m.Trailing())
|
||||
if len(fields) <= idx {
|
||||
|
@ -32,7 +32,7 @@ func init() {
|
|||
}
|
||||
})
|
||||
|
||||
tplFuncs.Register("tag", func(m *irc.Message, r *rule, fields map[string]interface{}) interface{} {
|
||||
tplFuncs.Register("tag", func(m *irc.Message, r *Rule, fields map[string]interface{}) interface{} {
|
||||
return func(tag string) string {
|
||||
s, _ := m.GetTag(tag)
|
||||
return s
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func formatMessage(tplString string, m *irc.Message, r *rule, fields map[string]interface{}) (string, error) {
|
||||
func formatMessage(tplString string, m *irc.Message, r *Rule, fields map[string]interface{}) (string, error) {
|
||||
compiledFields := map[string]interface{}{}
|
||||
|
||||
if config != nil {
|
||||
|
|
93
rule.go
93
rule.go
|
@ -1,7 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
@ -9,11 +11,12 @@ import (
|
|||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/go-irc/irc"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type rule struct {
|
||||
Actions []*ruleAction `yaml:"actions"`
|
||||
type Rule struct {
|
||||
Actions []*RuleAction `yaml:"actions"`
|
||||
|
||||
Cooldown *time.Duration `yaml:"cooldown"`
|
||||
ChannelCooldown *time.Duration `yaml:"channel_cooldown"`
|
||||
|
@ -38,7 +41,7 @@ type rule struct {
|
|||
disableOnMatchMessages []*regexp.Regexp
|
||||
}
|
||||
|
||||
func (r rule) MatcherID() string {
|
||||
func (r Rule) MatcherID() string {
|
||||
out := sha256.New()
|
||||
|
||||
for _, e := range []*string{
|
||||
|
@ -54,7 +57,7 @@ func (r rule) MatcherID() string {
|
|||
return fmt.Sprintf("sha256:%x", out.Sum(nil))
|
||||
}
|
||||
|
||||
func (r *rule) Matches(m *irc.Message, event *string) bool {
|
||||
func (r *Rule) matches(m *irc.Message, event *string) bool {
|
||||
var (
|
||||
badges = ircHandler{}.ParseBadgeLevels(m)
|
||||
logger = log.WithFields(log.Fields{
|
||||
|
@ -88,7 +91,7 @@ func (r *rule) Matches(m *irc.Message, event *string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) SetCooldown(m *irc.Message) {
|
||||
func (r *Rule) setCooldown(m *irc.Message) {
|
||||
if r.Cooldown != nil {
|
||||
timerStore.AddCooldown(timerTypeCooldown, "", r.MatcherID())
|
||||
}
|
||||
|
@ -102,7 +105,7 @@ func (r *rule) SetCooldown(m *irc.Message) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
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)
|
||||
|
@ -113,7 +116,7 @@ func (r *rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, eve
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteBadgeWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
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
|
||||
|
@ -128,7 +131,7 @@ func (r *rule) allowExecuteBadgeWhitelist(logger *log.Entry, m *irc.Message, eve
|
|||
return false
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteChannelCooldown(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
func (r *Rule) allowExecuteChannelCooldown(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
if r.ChannelCooldown == nil || len(m.Params) < 1 {
|
||||
// No match criteria set, does not speak against matching
|
||||
return true
|
||||
|
@ -147,7 +150,7 @@ func (r *rule) allowExecuteChannelCooldown(logger *log.Entry, m *irc.Message, ev
|
|||
return false
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteChannelWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
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
|
||||
|
@ -161,7 +164,7 @@ func (r *rule) allowExecuteChannelWhitelist(logger *log.Entry, m *irc.Message, e
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteDisable(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
func (r *Rule) allowExecuteDisable(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
if r.Disable == nil {
|
||||
// No match criteria set, does not speak against matching
|
||||
return true
|
||||
|
@ -175,7 +178,7 @@ func (r *rule) allowExecuteDisable(logger *log.Entry, m *irc.Message, event *str
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteDisableOnOffline(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
func (r *Rule) allowExecuteDisableOnOffline(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
if r.DisableOnOffline == nil || !*r.DisableOnOffline {
|
||||
// No match criteria set, does not speak against matching
|
||||
return true
|
||||
|
@ -194,7 +197,7 @@ func (r *rule) allowExecuteDisableOnOffline(logger *log.Entry, m *irc.Message, e
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteDisableOnPermit(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
func (r *Rule) allowExecuteDisableOnPermit(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
if r.DisableOnPermit != nil && *r.DisableOnPermit && timerStore.HasPermit(m.Params[0], m.User) {
|
||||
logger.Trace("Non-Match: Permit")
|
||||
return false
|
||||
|
@ -203,7 +206,7 @@ func (r *rule) allowExecuteDisableOnPermit(logger *log.Entry, m *irc.Message, ev
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteDisableOnTemplate(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
func (r *Rule) allowExecuteDisableOnTemplate(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
if r.DisableOnTemplate == nil {
|
||||
// No match criteria set, does not speak against matching
|
||||
return true
|
||||
|
@ -224,7 +227,7 @@ func (r *rule) allowExecuteDisableOnTemplate(logger *log.Entry, m *irc.Message,
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteEventWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
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
|
||||
|
@ -238,7 +241,7 @@ func (r *rule) allowExecuteEventWhitelist(logger *log.Entry, m *irc.Message, eve
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteMessageMatcherBlacklist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
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
|
||||
|
@ -267,7 +270,7 @@ func (r *rule) allowExecuteMessageMatcherBlacklist(logger *log.Entry, m *irc.Mes
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteMessageMatcherWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
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
|
||||
|
@ -292,7 +295,7 @@ func (r *rule) allowExecuteMessageMatcherWhitelist(logger *log.Entry, m *irc.Mes
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteRuleCooldown(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
func (r *Rule) allowExecuteRuleCooldown(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
|
||||
|
@ -311,7 +314,7 @@ func (r *rule) allowExecuteRuleCooldown(logger *log.Entry, m *irc.Message, event
|
|||
return false
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteUserCooldown(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
func (r *Rule) allowExecuteUserCooldown(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
if r.UserCooldown == nil {
|
||||
// No match criteria set, does not speak against matching
|
||||
return true
|
||||
|
@ -330,7 +333,7 @@ func (r *rule) allowExecuteUserCooldown(logger *log.Entry, m *irc.Message, event
|
|||
return false
|
||||
}
|
||||
|
||||
func (r *rule) allowExecuteUserWhitelist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool {
|
||||
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
|
||||
|
@ -344,28 +347,32 @@ func (r *rule) allowExecuteUserWhitelist(logger *log.Entry, m *irc.Message, even
|
|||
return true
|
||||
}
|
||||
|
||||
type ruleAction struct {
|
||||
Ban *string `json:"ban" yaml:"ban"`
|
||||
|
||||
Command []string `json:"command" yaml:"command"`
|
||||
|
||||
CounterSet *string `json:"counter_set" yaml:"counter_set"`
|
||||
CounterStep *int64 `json:"counter_step" yaml:"counter_step"`
|
||||
Counter *string `json:"counter" yaml:"counter"`
|
||||
|
||||
Delay time.Duration `json:"delay" yaml:"delay"`
|
||||
DelayJitter time.Duration `json:"delay_jitter" yaml:"delay_jitter"`
|
||||
|
||||
DeleteMessage *bool `json:"delete_message" yaml:"delete_message"`
|
||||
|
||||
RawMessage *string `json:"raw_message" yaml:"raw_message"`
|
||||
|
||||
Respond *string `json:"respond" yaml:"respond"`
|
||||
RespondAsReply *bool `json:"respond_as_reply" yaml:"respond_as_reply"`
|
||||
RespondFallback *string `json:"respond_fallback" yaml:"respond_fallback"`
|
||||
|
||||
Timeout *time.Duration `json:"timeout" yaml:"timeout"`
|
||||
|
||||
WhisperMessage *string `json:"whisper_message" yaml:"whisper_message"`
|
||||
WhisperTo *string `json:"whisper_to" yaml:"whisper_to"`
|
||||
type RuleAction struct {
|
||||
yamlUnmarshal func(interface{}) error
|
||||
jsonValue []byte
|
||||
}
|
||||
|
||||
func (r *RuleAction) UnmarshalJSON(d []byte) error {
|
||||
r.jsonValue = d
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RuleAction) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
r.yamlUnmarshal = unmarshal
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RuleAction) Unmarshal(v interface{}) error {
|
||||
switch {
|
||||
case r.yamlUnmarshal != nil:
|
||||
return r.yamlUnmarshal(v)
|
||||
|
||||
case r.jsonValue != nil:
|
||||
jd := json.NewDecoder(bytes.NewReader(r.jsonValue))
|
||||
jd.DisallowUnknownFields()
|
||||
return jd.Decode(v)
|
||||
|
||||
default:
|
||||
return errors.New("unmarshal on unprimed object")
|
||||
}
|
||||
}
|
||||
|
|
28
rule_test.go
28
rule_test.go
|
@ -16,7 +16,7 @@ var (
|
|||
)
|
||||
|
||||
func TestAllowExecuteBadgeBlacklist(t *testing.T) {
|
||||
r := &rule{DisableOn: []string{badgeBroadcaster}}
|
||||
r := &Rule{DisableOn: []string{badgeBroadcaster}}
|
||||
|
||||
if r.allowExecuteBadgeBlacklist(testLogger, nil, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) {
|
||||
t.Error("Execution allowed on blacklisted badge")
|
||||
|
@ -28,7 +28,7 @@ func TestAllowExecuteBadgeBlacklist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteBadgeWhitelist(t *testing.T) {
|
||||
r := &rule{EnableOn: []string{badgeBroadcaster}}
|
||||
r := &Rule{EnableOn: []string{badgeBroadcaster}}
|
||||
|
||||
if r.allowExecuteBadgeWhitelist(testLogger, nil, nil, badgeCollection{badgeModerator: testBadgeLevel0}) {
|
||||
t.Error("Execution allowed without whitelisted badge")
|
||||
|
@ -40,7 +40,7 @@ func TestAllowExecuteBadgeWhitelist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteChannelWhitelist(t *testing.T) {
|
||||
r := &rule{MatchChannels: []string{"#mychannel", "otherchannel"}}
|
||||
r := &Rule{MatchChannels: []string{"#mychannel", "otherchannel"}}
|
||||
|
||||
for m, exp := range map[string]bool{
|
||||
":amy!amy@foo.example.com PRIVMSG #mychannel :Testing": true,
|
||||
|
@ -59,7 +59,7 @@ func TestAllowExecuteChannelWhitelist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteDisable(t *testing.T) {
|
||||
for exp, r := range map[bool]*rule{
|
||||
for exp, r := range map[bool]*Rule{
|
||||
true: {Disable: testPtrBool(false)},
|
||||
false: {Disable: testPtrBool(true)},
|
||||
} {
|
||||
|
@ -70,7 +70,7 @@ func TestAllowExecuteDisable(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteDisableOnOffline(t *testing.T) {
|
||||
r := &rule{DisableOnOffline: testPtrBool(true)}
|
||||
r := &Rule{DisableOnOffline: testPtrBool(true)}
|
||||
|
||||
// Fake cache entries to prevent calling the real Twitch API
|
||||
twitch.apiCache.Set([]string{"hasLiveStream", "channel1"}, time.Minute, true)
|
||||
|
@ -87,7 +87,7 @@ func TestAllowExecuteDisableOnOffline(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteChannelCooldown(t *testing.T) {
|
||||
r := &rule{ChannelCooldown: func(i time.Duration) *time.Duration { return &i }(time.Minute), SkipCooldownFor: []string{badgeBroadcaster}}
|
||||
r := &Rule{ChannelCooldown: func(i time.Duration) *time.Duration { return &i }(time.Minute), SkipCooldownFor: []string{badgeBroadcaster}}
|
||||
c1 := irc.MustParseMessage(":amy!amy@foo.example.com PRIVMSG #mychannel :Testing")
|
||||
c2 := irc.MustParseMessage(":amy!amy@foo.example.com PRIVMSG #otherchannel :Testing")
|
||||
|
||||
|
@ -112,7 +112,7 @@ func TestAllowExecuteChannelCooldown(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteDisableOnPermit(t *testing.T) {
|
||||
r := &rule{DisableOnPermit: testPtrBool(true)}
|
||||
r := &Rule{DisableOnPermit: testPtrBool(true)}
|
||||
|
||||
// Permit is using global configuration, so we must fake that one
|
||||
config = &configFile{PermitTimeout: time.Minute}
|
||||
|
@ -130,7 +130,7 @@ func TestAllowExecuteDisableOnPermit(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteDisableOnTemplate(t *testing.T) {
|
||||
r := &rule{DisableOnTemplate: func(s string) *string { return &s }(`{{ ne .username "amy" }}`)}
|
||||
r := &Rule{DisableOnTemplate: func(s string) *string { return &s }(`{{ ne .username "amy" }}`)}
|
||||
|
||||
for msg, exp := range map[string]bool{
|
||||
":amy!amy@foo.example.com PRIVMSG #mychannel :Testing": true,
|
||||
|
@ -143,7 +143,7 @@ func TestAllowExecuteDisableOnTemplate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteEventWhitelist(t *testing.T) {
|
||||
r := &rule{MatchEvent: func(s string) *string { return &s }("test")}
|
||||
r := &Rule{MatchEvent: func(s string) *string { return &s }("test")}
|
||||
|
||||
for evt, exp := range map[string]bool{
|
||||
"foobar": false,
|
||||
|
@ -156,7 +156,7 @@ func TestAllowExecuteEventWhitelist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteMessageMatcherBlacklist(t *testing.T) {
|
||||
r := &rule{DisableOnMatchMessages: []string{`^!disable`}}
|
||||
r := &Rule{DisableOnMatchMessages: []string{`^!disable`}}
|
||||
|
||||
for msg, exp := range map[string]bool{
|
||||
"PRIVMSG #test :Random message": true,
|
||||
|
@ -169,7 +169,7 @@ func TestAllowExecuteMessageMatcherBlacklist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteMessageMatcherWhitelist(t *testing.T) {
|
||||
r := &rule{MatchMessage: func(s string) *string { return &s }(`^!test`)}
|
||||
r := &Rule{MatchMessage: func(s string) *string { return &s }(`^!test`)}
|
||||
|
||||
for msg, exp := range map[string]bool{
|
||||
"PRIVMSG #test :Random message": false,
|
||||
|
@ -182,7 +182,7 @@ func TestAllowExecuteMessageMatcherWhitelist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteRuleCooldown(t *testing.T) {
|
||||
r := &rule{Cooldown: func(i time.Duration) *time.Duration { return &i }(time.Minute), SkipCooldownFor: []string{badgeBroadcaster}}
|
||||
r := &Rule{Cooldown: func(i time.Duration) *time.Duration { return &i }(time.Minute), SkipCooldownFor: []string{badgeBroadcaster}}
|
||||
|
||||
if !r.allowExecuteRuleCooldown(testLogger, nil, nil, badgeCollection{}) {
|
||||
t.Error("Initial call was not allowed")
|
||||
|
@ -201,7 +201,7 @@ func TestAllowExecuteRuleCooldown(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteUserCooldown(t *testing.T) {
|
||||
r := &rule{UserCooldown: func(i time.Duration) *time.Duration { return &i }(time.Minute), SkipCooldownFor: []string{badgeBroadcaster}}
|
||||
r := &Rule{UserCooldown: func(i time.Duration) *time.Duration { return &i }(time.Minute), SkipCooldownFor: []string{badgeBroadcaster}}
|
||||
c1 := irc.MustParseMessage(":ben!ben@foo.example.com PRIVMSG #mychannel :Testing")
|
||||
c2 := irc.MustParseMessage(":amy!amy@foo.example.com PRIVMSG #mychannel :Testing")
|
||||
|
||||
|
@ -226,7 +226,7 @@ func TestAllowExecuteUserCooldown(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllowExecuteUserWhitelist(t *testing.T) {
|
||||
r := &rule{MatchUsers: []string{"amy"}}
|
||||
r := &Rule{MatchUsers: []string{"amy"}}
|
||||
|
||||
for msg, exp := range map[string]bool{
|
||||
":amy!amy@foo.example.com PRIVMSG #mychannel :Testing": true,
|
||||
|
|
Loading…
Reference in a new issue