Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add global and channel rate limits #1065

Merged
merged 7 commits into from
May 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor

@maxpowa maxpowa Apr 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing rate will break backwards compatibility with jenni modules. Maybe alias rate to user_rate in the getattr?

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