Use more flexible Actor format to allow addition of new actors (#5)

This commit is contained in:
Knut Ahlers 2021-06-11 13:52:42 +02:00 committed by GitHub
parent 0db778f841
commit ede8a95ed4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 453 additions and 327 deletions

View File

@ -8,20 +8,29 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if r.Ban == nil {
return nil
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
fmt.Sprintf("/ban %s %s", m.User, *r.Ban),
},
}),
"sending timeout",
)
})
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
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
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" }

View File

@ -8,41 +8,52 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if r.Counter == nil {
return nil
}
registerAction(func() Actor { return &ActorCounter{} })
}
counterName, err := formatMessage(*r.Counter, m, ruleDef, nil)
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(*a.Counter, m, r, nil)
if err != nil {
return errors.Wrap(err, "preparing response")
}
if a.CounterSet != nil {
parseValue, err := formatMessage(*a.CounterSet, m, r, nil)
if err != nil {
return errors.Wrap(err, "preparing response")
return errors.Wrap(err, "execute counter value template")
}
if r.CounterSet != nil {
parseValue, err := formatMessage(*r.CounterSet, m, ruleDef, nil)
if err != nil {
return errors.Wrap(err, "execute counter value template")
}
counterValue, err := strconv.ParseInt(parseValue, 10, 64)
if err != nil {
return errors.Wrap(err, "parse counter value")
}
return errors.Wrap(
store.UpdateCounter(counterName, counterValue, true),
"set counter",
)
}
var counterStep int64 = 1
if r.CounterStep != nil {
counterStep = *r.CounterStep
counterValue, err := strconv.ParseInt(parseValue, 10, 64)
if err != nil {
return errors.Wrap(err, "parse counter value")
}
return errors.Wrap(
store.UpdateCounter(counterName, counterStep, false),
"update counter",
store.UpdateCounter(counterName, counterValue, true),
"set counter",
)
})
}
var counterStep int64 = 1
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" }

View File

@ -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 {
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
}
time.Sleep(totalDelay)
return nil
})
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 := 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" }

View File

@ -8,25 +8,34 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if r.DeleteMessage == nil || !*r.DeleteMessage {
return nil
}
msgID, ok := m.Tags.GetTag("id")
if !ok || msgID == "" {
return nil
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
fmt.Sprintf("/delete %s", msgID),
},
}),
"sending delete",
)
})
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
}
msgID, ok := m.Tags.GetTag("id")
if !ok || msgID == "" {
return nil
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
fmt.Sprintf("/delete %s", msgID),
},
}),
"sending delete",
)
}
func (a ActorDelete) IsAsync() bool { return false }
func (a ActorDelete) Name() string { return "delete" }

View File

@ -6,24 +6,33 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if r.RawMessage == nil {
return nil
}
rawMsg, err := formatMessage(*r.RawMessage, m, ruleDef, nil)
if err != nil {
return errors.Wrap(err, "preparing raw message")
}
msg, err := irc.ParseMessage(rawMsg)
if err != nil {
return errors.Wrap(err, "parsing raw message")
}
return errors.Wrap(
c.WriteMessage(msg),
"sending raw message",
)
})
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(*a.RawMessage, m, r, nil)
if err != nil {
return errors.Wrap(err, "preparing raw message")
}
msg, err := irc.ParseMessage(rawMsg)
if err != nil {
return errors.Wrap(err, "parsing raw message")
}
return errors.Wrap(
c.WriteMessage(msg),
"sending raw message",
)
}
func (a ActorRaw) IsAsync() bool { return false }
func (a ActorRaw) Name() string { return "raw" }

View File

@ -6,42 +6,53 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if r.Respond == nil {
return nil
}
msg, err := formatMessage(*r.Respond, m, ruleDef, nil)
if err != nil {
if r.RespondFallback == nil {
return errors.Wrap(err, "preparing response")
}
if msg, err = formatMessage(*r.RespondFallback, m, ruleDef, nil); err != nil {
return errors.Wrap(err, "preparing response fallback")
}
}
ircMessage := &irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
msg,
},
}
if r.RespondAsReply != nil && *r.RespondAsReply {
id, ok := m.GetTag("id")
if ok {
if ircMessage.Tags == nil {
ircMessage.Tags = make(irc.Tags)
}
ircMessage.Tags["reply-parent-msg-id"] = irc.TagValue(id)
}
}
return errors.Wrap(
c.WriteMessage(ircMessage),
"sending response",
)
})
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(*a.Respond, m, r, nil)
if err != nil {
if a.RespondFallback == nil {
return errors.Wrap(err, "preparing response")
}
if msg, err = formatMessage(*a.RespondFallback, m, r, nil); err != nil {
return errors.Wrap(err, "preparing response fallback")
}
}
ircMessage := &irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
msg,
},
}
if a.RespondAsReply != nil && *a.RespondAsReply {
id, ok := m.GetTag("id")
if ok {
if ircMessage.Tags == nil {
ircMessage.Tags = make(irc.Tags)
}
ircMessage.Tags["reply-parent-msg-id"] = irc.TagValue(id)
}
}
return errors.Wrap(
c.WriteMessage(ircMessage),
"sending response",
)
}
func (a ActorRespond) IsAsync() bool { return false }
func (a ActorRespond) Name() string { return "respond" }

View File

@ -12,70 +12,79 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if len(r.Command) == 0 {
return nil
}
var command []string
for _, arg := range r.Command {
tmp, err := formatMessage(arg, m, ruleDef, nil)
if err != nil {
return errors.Wrap(err, "execute command argument template")
}
command = append(command, tmp)
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.CommandTimeout)
defer cancel()
var (
stdin = new(bytes.Buffer)
stdout = new(bytes.Buffer)
)
if err := json.NewEncoder(stdin).Encode(map[string]interface{}{
"badges": ircHandler{}.ParseBadgeLevels(m),
"channel": m.Params[0],
"message": m.Trailing(),
"tags": m.Tags,
"username": m.User,
}); err != nil {
return errors.Wrap(err, "encoding script input")
}
cmd := exec.CommandContext(ctx, command[0], command[1:]...) // #nosec G204 // This is expected to call a command with parameters
cmd.Env = os.Environ()
cmd.Stderr = os.Stderr
cmd.Stdin = stdin
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "running command")
}
if stdout.Len() == 0 {
// Script was successful but did not yield actions
return nil
}
var (
actions []*ruleAction
decoder = json.NewDecoder(stdout)
)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&actions); err != nil {
return errors.Wrap(err, "decoding actions output")
}
for _, action := range actions {
if err := triggerActions(c, m, ruleDef, action); err != nil {
return errors.Wrap(err, "execute returned action")
}
}
return nil
})
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 a.Command {
tmp, err := formatMessage(arg, m, r, nil)
if err != nil {
return errors.Wrap(err, "execute command argument template")
}
command = append(command, tmp)
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.CommandTimeout)
defer cancel()
var (
stdin = new(bytes.Buffer)
stdout = new(bytes.Buffer)
)
if err := json.NewEncoder(stdin).Encode(map[string]interface{}{
"badges": ircHandler{}.ParseBadgeLevels(m),
"channel": m.Params[0],
"message": m.Trailing(),
"tags": m.Tags,
"username": m.User,
}); err != nil {
return errors.Wrap(err, "encoding script input")
}
cmd := exec.CommandContext(ctx, command[0], command[1:]...) // #nosec G204 // This is expected to call a command with parameters
cmd.Env = os.Environ()
cmd.Stderr = os.Stderr
cmd.Stdin = stdin
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "running command")
}
if stdout.Len() == 0 {
// Script was successful but did not yield actions
return nil
}
var (
actions []*RuleAction
decoder = json.NewDecoder(stdout)
)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&actions); err != nil {
return errors.Wrap(err, "decoding actions output")
}
for _, action := range actions {
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" }

View File

@ -9,20 +9,29 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if r.Timeout == nil {
return nil
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
fmt.Sprintf("/timeout %s %d", m.User, *r.Timeout/time.Second),
},
}),
"sending timeout",
)
})
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
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
m.Params[0],
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" }

View File

@ -8,35 +8,45 @@ import (
)
func init() {
registerAction(func(c *irc.Client, m *irc.Message, ruleDef *rule, r *ruleAction) error {
if r.WhisperTo == nil || r.WhisperMessage == nil {
return nil
}
to, err := formatMessage(*r.WhisperTo, m, ruleDef, nil)
if err != nil {
return errors.Wrap(err, "preparing whisper receiver")
}
msg, err := formatMessage(*r.WhisperMessage, m, ruleDef, nil)
if err != nil {
return errors.Wrap(err, "preparing whisper message")
}
channel := "#tmijs" // As a fallback, copied from tmi.js
if len(config.Channels) > 0 {
channel = fmt.Sprintf("#%s", config.Channels[0])
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
channel,
fmt.Sprintf("/w %s %s", to, msg),
},
}),
"sending whisper",
)
})
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(*a.WhisperTo, m, r, nil)
if err != nil {
return errors.Wrap(err, "preparing whisper receiver")
}
msg, err := formatMessage(*a.WhisperMessage, m, r, nil)
if err != nil {
return errors.Wrap(err, "preparing whisper message")
}
channel := "#tmijs" // As a fallback, copied from tmi.js
if len(config.Channels) > 0 {
channel = fmt.Sprintf("#%s", config.Channels[0])
}
return errors.Wrap(
c.WriteMessage(&irc.Message{
Command: "PRIVMSG",
Params: []string{
channel,
fmt.Sprintf("/w %s %s", to, msg),
},
}),
"sending whisper",
)
}
func (a ActorWhisper) IsAsync() bool { return false }
func (a ActorWhisper) Name() string { return "whisper" }

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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() {

View File

@ -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 {

View File

@ -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

View File

@ -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
View File

@ -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")
}
}

View File

@ -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,