Add user- and channel-based cooldowns (#4)

This commit is contained in:
Knut Ahlers 2021-06-07 22:20:19 +02:00 committed by GitHub
parent d62985feb4
commit 0db778f841
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 166 additions and 47 deletions

View file

@ -44,6 +44,6 @@ func handleMessage(c *irc.Client, m *irc.Message, event *string) {
}
// Lock command
timerStore.AddCooldown(r.MatcherID())
r.SetCooldown(m)
}
}

96
rule.go
View file

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

View file

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

View file

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

View file

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