Skip to content

Commit

Permalink
Add global and channel rate limits (#1065)
Browse files Browse the repository at this point in the history
* Add global and channel rate limits

* Default user rate and compatibility with jenni modules

* Fix critical keyerror bug in rate limiting

* Simplify syntax for @Rate() decorator and update docs

* Don't reset function timer during cooldown

* fix channel time diff variable

* fix indentation in bot.py
  • Loading branch information
calebj authored and maxpowa committed May 19, 2016
1 parent 02c2e59 commit 28d4806
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 23 deletions.
57 changes: 42 additions & 15 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,22 +420,46 @@ def reply(self, message, destination=None, reply_to=None, notice=False):

def call(self, func, sopel, trigger):
nick = trigger.nick
current_time = time.time()
if nick not in self._times:
self._times[nick] = dict()

if not trigger.admin and \
not func.unblockable and \
func.rate > 0 and \
func in self._times[nick]:
timediff = time.time() - self._times[nick][func]
if timediff < func.rate:
self._times[nick][func] = time.time()
LOGGER.info(
"%s prevented from using %s in %s: %d < %d",
trigger.nick, func.__name__, trigger.sender, timediff,
func.rate
)
return
if self.nick not in self._times:
self._times[self.nick] = dict()
if not trigger.is_privmsg and trigger.sender not in self._times:
self._times[trigger.sender] = dict()

if not trigger.admin and not func.unblockable:
if func in self._times[nick]:
usertimediff = current_time - self._times[nick][func]
if func.rate > 0 and usertimediff < func.rate:
#self._times[nick][func] = current_time
LOGGER.info(
"%s prevented from using %s in %s due to user limit: %d < %d",
trigger.nick, func.__name__, trigger.sender, usertimediff,
func.rate
)
return
if func in self._times[self.nick]:
globaltimediff = current_time - self._times[self.nick][func]
if func.global_rate > 0 and globaltimediff < func.global_rate:
#self._times[self.nick][func] = current_time
LOGGER.info(
"%s prevented from using %s in %s due to global limit: %d < %d",
trigger.nick, func.__name__, trigger.sender, globaltimediff,
func.global_rate
)
return

if not trigger.is_privmsg and func in self._times[trigger.sender]:
chantimediff = current_time - self._times[trigger.sender][func]
if func.channel_rate > 0 and chantimediff < func.channel_rate:
#self._times[trigger.sender][func] = current_time
LOGGER.info(
"%s prevented from using %s in %s due to channel limit: %d < %d",
trigger.nick, func.__name__, trigger.sender, chantimediff,
func.channel_rate
)
return

try:
exit_code = func(sopel, trigger)
Expand All @@ -444,7 +468,10 @@ def call(self, func, sopel, trigger):
self.error(trigger)

if exit_code != NOLIMIT:
self._times[nick][func] = time.time()
self._times[nick][func] = current_time
self._times[self.nick][func] = current_time
if not trigger.is_privmsg:
self._times[trigger.sender][func] = current_time

def dispatch(self, pretrigger):
args = pretrigger.args
Expand Down
2 changes: 2 additions & 0 deletions sopel/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ def clean_callable(func, config):
func.priority = getattr(func, 'priority', 'medium')
func.thread = getattr(func, 'thread', True)
func.rate = getattr(func, 'rate', 0)
func.channel_rate = getattr(func, 'channel_rate', 0)
func.global_rate = getattr(func, 'global_rate', 0)

if not hasattr(func, 'event'):
func.event = ['PRIVMSG']
Expand Down
18 changes: 10 additions & 8 deletions sopel/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,19 +248,21 @@ def add_attribute(function):
return add_attribute


def rate(value):
"""Decorate a function to limit how often a single user may trigger it.
If a function is given a rate of 20, a single user may only use that
function once every 20 seconds. This limit applies to each user
individually. Users on the admin list in Sopel’s configuration are exempted
from rate limits.
def rate(user=0, channel=0, server=0):
"""Decorate a function to limit how often it can be triggered on a per-user
basis, in a channel, or across the server (bot). A value of zero means no
limit. If a function is given a rate of 20, that function may only be used
once every 20 seconds in the scope corresponding to the parameter.
Users on the admin list in Sopel’s configuration are exempted from rate
limits.
Rate-limited functions that use scheduled future commands should import
threading.Timer() instead of sched, or rate limiting will not work properly.
"""
def add_attribute(function):
function.rate = value
function.rate = user
function.channel_rate = channel
function.global_rate = server
return function
return add_attribute

Expand Down

0 comments on commit 28d4806

Please sign in to comment.