diff --git a/actions.go b/actions.go index 6187f6b..49dff28 100644 --- a/actions.go +++ b/actions.go @@ -44,6 +44,6 @@ func handleMessage(c *irc.Client, m *irc.Message, event *string) { } // Lock command - timerStore.AddCooldown(r.MatcherID()) + r.SetCooldown(m) } } diff --git a/rule.go b/rule.go index ec98af6..a9941e5 100644 --- a/rule.go +++ b/rule.go @@ -16,6 +16,8 @@ type rule struct { Actions []*ruleAction `yaml:"actions"` Cooldown *time.Duration `yaml:"cooldown"` + ChannelCooldown *time.Duration `yaml:"channel_cooldown"` + UserCooldown *time.Duration `yaml:"user_cooldown"` SkipCooldownFor []string `yaml:"skip_cooldown_for"` MatchChannels []string `yaml:"match_channels"` @@ -71,7 +73,9 @@ func (r *rule) Matches(m *irc.Message, event *string) bool { r.allowExecuteBadgeBlacklist, r.allowExecuteBadgeWhitelist, r.allowExecuteDisableOnPermit, - r.allowExecuteCooldown, + r.allowExecuteRuleCooldown, + r.allowExecuteChannelCooldown, + r.allowExecuteUserCooldown, r.allowExecuteDisableOnTemplate, r.allowExecuteDisableOnOffline, } { @@ -84,6 +88,20 @@ func (r *rule) Matches(m *irc.Message, event *string) bool { return true } +func (r *rule) SetCooldown(m *irc.Message) { + if r.Cooldown != nil { + timerStore.AddCooldown(timerTypeCooldown, "", r.MatcherID()) + } + + if r.ChannelCooldown != nil && len(m.Params) > 0 { + timerStore.AddCooldown(timerTypeCooldown, m.Params[0], r.MatcherID()) + } + + if r.UserCooldown != nil { + timerStore.AddCooldown(timerTypeCooldown, m.User, r.MatcherID()) + } +} + func (r *rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges badgeCollection) bool { for _, b := range r.DisableOn { if badges.Has(b) { @@ -110,6 +128,25 @@ 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 { + if r.ChannelCooldown == nil || len(m.Params) < 1 { + // No match criteria set, does not speak against matching + return true + } + + if !timerStore.InCooldown(timerTypeCooldown, m.Params[0], r.MatcherID(), *r.ChannelCooldown) { + return true + } + + for _, b := range r.SkipCooldownFor { + if badges.Has(b) { + return true + } + } + + 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 @@ -124,25 +161,6 @@ func (r *rule) allowExecuteChannelWhitelist(logger *log.Entry, m *irc.Message, e 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) 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 @@ -274,6 +292,44 @@ 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 { + if r.Cooldown == nil { + // No match criteria set, does not speak against matching + return true + } + + if !timerStore.InCooldown(timerTypeCooldown, "", r.MatcherID(), *r.Cooldown) { + return true + } + + for _, b := range r.SkipCooldownFor { + if badges.Has(b) { + return true + } + } + + return false +} + +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 + } + + if !timerStore.InCooldown(timerTypeCooldown, m.User, r.MatcherID(), *r.UserCooldown) { + return true + } + + for _, b := range r.SkipCooldownFor { + if badges.Has(b) { + return true + } + } + + return false +} + 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 diff --git a/rule_test.go b/rule_test.go index 1f8fb87..16bcb11 100644 --- a/rule_test.go +++ b/rule_test.go @@ -58,25 +58,6 @@ func TestAllowExecuteChannelWhitelist(t *testing.T) { } } -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 TestAllowExecuteDisable(t *testing.T) { for exp, r := range map[bool]*rule{ true: {Disable: testPtrBool(false)}, @@ -105,6 +86,31 @@ 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}} + c1 := irc.MustParseMessage(":amy!amy@foo.example.com PRIVMSG #mychannel :Testing") + c2 := irc.MustParseMessage(":amy!amy@foo.example.com PRIVMSG #otherchannel :Testing") + + if !r.allowExecuteChannelCooldown(testLogger, c1, nil, badgeCollection{}) { + t.Error("Initial call was not allowed") + } + + // Add cooldown + timerStore.AddCooldown(timerTypeCooldown, c1.Params[0], r.MatcherID()) + + if r.allowExecuteChannelCooldown(testLogger, c1, nil, badgeCollection{}) { + t.Error("Call after cooldown added was allowed") + } + + if !r.allowExecuteChannelCooldown(testLogger, c1, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Call in cooldown with skip badge was not allowed") + } + + if !r.allowExecuteChannelCooldown(testLogger, c2, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Call in cooldown with different channel was not allowed") + } +} + func TestAllowExecuteDisableOnPermit(t *testing.T) { r := &rule{DisableOnPermit: testPtrBool(true)} @@ -175,6 +181,50 @@ 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}} + + if !r.allowExecuteRuleCooldown(testLogger, nil, nil, badgeCollection{}) { + t.Error("Initial call was not allowed") + } + + // Add cooldown + timerStore.AddCooldown(timerTypeCooldown, "", r.MatcherID()) + + if r.allowExecuteRuleCooldown(testLogger, nil, nil, badgeCollection{}) { + t.Error("Call after cooldown added was allowed") + } + + if !r.allowExecuteRuleCooldown(testLogger, nil, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Call in cooldown with skip badge was not allowed") + } +} + +func TestAllowExecuteUserCooldown(t *testing.T) { + 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") + + if !r.allowExecuteUserCooldown(testLogger, c1, nil, badgeCollection{}) { + t.Error("Initial call was not allowed") + } + + // Add cooldown + timerStore.AddCooldown(timerTypeCooldown, c1.User, r.MatcherID()) + + if r.allowExecuteUserCooldown(testLogger, c1, nil, badgeCollection{}) { + t.Error("Call after cooldown added was allowed") + } + + if !r.allowExecuteUserCooldown(testLogger, c1, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Call in cooldown with skip badge was not allowed") + } + + if !r.allowExecuteUserCooldown(testLogger, c2, nil, badgeCollection{badgeBroadcaster: testBadgeLevel0}) { + t.Error("Call in cooldown with different user was not allowed") + } +} + func TestAllowExecuteUserWhitelist(t *testing.T) { r := &rule{MatchUsers: []string{"amy"}} diff --git a/timers.go b/timers.go index f4ca7d6..78eff7c 100644 --- a/timers.go +++ b/timers.go @@ -36,17 +36,17 @@ func newTimer() *timer { // Cooldown timer -func (t *timer) AddCooldown(ruleID string) { - t.add(timerTypeCooldown, t.getCooldownTimerKey(ruleID)) +func (t *timer) AddCooldown(tt timerType, limiter, ruleID string) { + t.add(timerTypeCooldown, t.getCooldownTimerKey(tt, limiter, ruleID)) } -func (t *timer) InCooldown(ruleID string, cooldown time.Duration) bool { - return t.has(t.getCooldownTimerKey(ruleID), cooldown) +func (t *timer) InCooldown(tt timerType, limiter, ruleID string, cooldown time.Duration) bool { + return t.has(t.getCooldownTimerKey(tt, limiter, ruleID), cooldown) } -func (t timer) getCooldownTimerKey(ruleID string) string { +func (t timer) getCooldownTimerKey(tt timerType, limiter, ruleID string) string { h := sha256.New() - fmt.Fprintf(h, "%d:%s", timerTypeCooldown, ruleID) + fmt.Fprintf(h, "%d:%s:%s", tt, limiter, ruleID) return fmt.Sprintf("sha256:%x", h.Sum(nil)) } diff --git a/wiki/Home.md b/wiki/Home.md index dc79c3b..553774e 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -75,9 +75,22 @@ rules: # See below for examples - whisper_to: '{{ .username }}' # String, username to send to, applies templating whisper_message: 'Ohai!' # String, message to send, applies templating - # Add a cooldown to the command (not to trigger counters twice, ...) + # Add a cooldown to the rule in general (not to trigger counters twice, ...) + # Using this will prevent the rule to be executed in all matching channels + # as long as the cooldown is active. cooldown: 1s # Duration value: 1s / 1m / 1h + # Add a cooldown to the rule per channel (not to trigger counters twice, ...) + # Using this will prevent the rule to be executed in the channel it was triggered + # which means other channels are not affected. + channel_cooldown: 1s # Duration value: 1s / 1m / 1h + + # Add a cooldown to the rule per user (not to trigger counters twice, ...) + # Using this will prevent the rule to be executed for the user which triggered it + # in any of the matching channels, which means other users can trigger the command + # while that particular user cannot + user_cooldown: 1s # Duration value: 1s / 1m / 1h + # Do not apply cooldown for these badges skip_cooldown_for: [broadcaster, moderator]