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

bot, plugin, plugin.rules: optionally include admins in rate limits too #2652

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,9 @@ def rate_limit_info(
rule: AbstractRuleType,
trigger: Trigger,
) -> tuple[bool, Optional[str]]:
if trigger.admin or rule.is_unblockable():
if rule.is_unblockable():
return False, None
if trigger.admin and not rule.is_admin_rate_limited():
return False, None

nick = trigger.nick
Expand Down
16 changes: 16 additions & 0 deletions sopel/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,7 @@ def rate(
server: int = 0,
*,
message: Optional[str] = None,
include_admins: Optional[bool] = False,
) -> Callable:
"""Decorate a function to be rate-limited.

Expand All @@ -1146,6 +1147,8 @@ def rate(
who triggered it or where
:param message: optional keyword argument; default message sent as NOTICE
when a rate limit is reached
:param include_admins: optional boolean to include admins in the rate limit
(default ``False``)

How often a function can be triggered on a per-user basis, in a channel,
or across the server (bot) can be controlled with this decorator. A value
Expand Down Expand Up @@ -1217,19 +1220,23 @@ def add_attribute(function):
if not hasattr(function, 'global_rate'):
function.global_rate = server
function.default_rate_message = message
function.rate_limit_admins = include_admins
return function
return add_attribute


def rate_user(
rate: int,
message: Optional[str] = None,
include_admins: Optional[bool] = False,
) -> Callable:
"""Decorate a function to be rate-limited for a user.

:param rate: seconds between permitted calls of this function by the same
user
:param message: optional; message sent as NOTICE when a user hits the limit
:param include_admins: optional boolean to include admins in the rate limit
(default ``False``)

This decorator can be used alone or with the :func:`rate` decorator, as it
will always take precedence::
Expand Down Expand Up @@ -1258,19 +1265,23 @@ def rate_user(
def add_attribute(function):
function.user_rate = rate
function.user_rate_message = message
function.rate_limit_admins = include_admins
return function
return add_attribute


def rate_channel(
rate: int,
message: Optional[str] = None,
include_admins: Optional[bool] = False,
) -> Callable:
"""Decorate a function to be rate-limited for a channel.

:param rate: seconds between permitted calls of this function in the same
channel, regardless of triggering user
:param message: optional; message sent as NOTICE when a user hits the limit
:param include_admins: optional boolean to include admins in the rate limit
(default ``False``)

This decorator can be used alone or with the :func:`rate` decorator, as it
will always take precedence::
Expand Down Expand Up @@ -1302,19 +1313,23 @@ def rate_channel(
def add_attribute(function):
function.channel_rate = rate
function.channel_rate_message = message
function.rate_limit_admins = include_admins
return function
return add_attribute


def rate_global(
rate: int,
message: Optional[str] = None,
include_admins: Optional[bool] = False,
) -> Callable:
"""Decorate a function to be rate-limited for the whole server.

:param rate: seconds between permitted calls of this function no matter who
triggered it or where
:param message: optional; message sent as NOTICE when a user hits the limit
:param include_admins: optional boolean to include admins in the rate limit
(default ``False``)

This decorator can be used alone or with the :func:`rate` decorator, as it
will always take precedence.
Expand Down Expand Up @@ -1345,6 +1360,7 @@ def rate_global(
def add_attribute(function):
function.global_rate = rate
function.global_rate_message = message
function.rate_limit_admins = include_admins
return function
return add_attribute

Expand Down
13 changes: 13 additions & 0 deletions sopel/plugins/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,13 @@ def is_unblockable(self) -> bool:
:return: ``True`` when the rule is unblockable, ``False`` otherwise
"""

@abc.abstractmethod
def is_admin_rate_limited(self) -> bool:
"""Tell if admins should be included in this rule's rate limits.

:return: ``True`` when admins should be rate limited, else ``False``
"""

@abc.abstractmethod
def get_user_metrics(self, nick: Identifier) -> RuleMetrics:
"""Get the rule's usage metrics for the given user."""
Expand Down Expand Up @@ -929,6 +936,7 @@ def kwargs_from_callable(cls, handler):
'threaded': getattr(handler, 'thread', True),
'output_prefix': getattr(handler, 'output_prefix', ''),
'unblockable': getattr(handler, 'unblockable', False),
'rate_limit_admins': getattr(handler, 'rate_limit_admins', False),
'user_rate_limit': getattr(handler, 'user_rate', 0),
'channel_rate_limit': getattr(handler, 'channel_rate', 0),
'global_rate_limit': getattr(handler, 'global_rate', 0),
Expand Down Expand Up @@ -1027,6 +1035,7 @@ def __init__(self,
threaded=True,
output_prefix=None,
unblockable=False,
rate_limit_admins=False,
user_rate_limit=0,
channel_rate_limit=0,
global_rate_limit=0,
Expand Down Expand Up @@ -1056,6 +1065,7 @@ def __init__(self,

# rate limiting
self._unblockable = bool(unblockable)
self._rate_limit_admins = bool(rate_limit_admins)
self._user_rate_limit = user_rate_limit
self._channel_rate_limit = channel_rate_limit
self._global_rate_limit = global_rate_limit
Expand Down Expand Up @@ -1194,6 +1204,9 @@ def is_threaded(self):
def is_unblockable(self):
return self._unblockable

def is_admin_rate_limited(self):
return self._rate_limit_admins

def get_user_metrics(self, nick: Identifier) -> RuleMetrics:
return self._metrics_nick.get(nick, RuleMetrics())

Expand Down
30 changes: 26 additions & 4 deletions test/plugins/test_plugins_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,16 @@ def test_rule_unblockable():
assert rule.is_unblockable()


def test_rule_rate_limit_admins():
regex = re.compile('.*')

rule = rules.Rule([regex])
assert not rule.is_admin_rate_limited()

rule = rules.Rule([regex], rate_limit_admins=True)
assert rule.is_admin_rate_limited()


def test_rule_parse_wildcard():
# match everything
regex = re.compile(r'.*')
Expand Down Expand Up @@ -1109,6 +1119,7 @@ def handler(wrapped, trigger):
assert 'threaded' in kwargs
assert 'output_prefix' in kwargs
assert 'unblockable' in kwargs
assert 'rate_limit_admins' in kwargs
assert 'user_rate_limit' in kwargs
assert 'channel_rate_limit' in kwargs
assert 'global_rate_limit' in kwargs
Expand All @@ -1129,6 +1140,7 @@ def handler(wrapped, trigger):
assert kwargs['threaded'] is True
assert kwargs['output_prefix'] == ''
assert kwargs['unblockable'] is False
assert kwargs['rate_limit_admins'] is False
assert kwargs['user_rate_limit'] == 0
assert kwargs['channel_rate_limit'] == 0
assert kwargs['global_rate_limit'] == 0
Expand Down Expand Up @@ -1249,21 +1261,25 @@ def handler(wrapped, trigger):
def test_kwargs_from_callable_rate_limit(mockbot):
# prepare callable
@plugin.rule(r'hello', r'hi', r'hey', r'hello|hi')
@plugin.rate(user=20, channel=30, server=40, message='Default message.')
@plugin.rate(
user=20, channel=30, server=40, message='Default message.',
include_admins=True)
def handler(wrapped, trigger):
wrapped.reply('Hi!')

loader.clean_callable(handler, mockbot.settings)

# get kwargs
kwargs = rules.Rule.kwargs_from_callable(handler)
assert 'rate_limit_admins' in kwargs
assert 'user_rate_limit' in kwargs
assert 'channel_rate_limit' in kwargs
assert 'global_rate_limit' in kwargs
assert 'user_rate_message' in kwargs
assert 'channel_rate_message' in kwargs
assert 'global_rate_message' in kwargs
assert 'default_rate_message' in kwargs
assert kwargs['rate_limit_admins'] is True
assert kwargs['user_rate_limit'] == 20
assert kwargs['channel_rate_limit'] == 30
assert kwargs['global_rate_limit'] == 40
Expand All @@ -1276,21 +1292,23 @@ def handler(wrapped, trigger):
def test_kwargs_from_callable_rate_limit_user(mockbot):
# prepare callable
@plugin.rule(r'hello', r'hi', r'hey', r'hello|hi')
@plugin.rate_user(20, 'User message.')
@plugin.rate_user(20, 'User message.', True)
def handler(wrapped, trigger):
wrapped.reply('Hi!')

loader.clean_callable(handler, mockbot.settings)

# get kwargs
kwargs = rules.Rule.kwargs_from_callable(handler)
assert 'rate_limit_admins' in kwargs
assert 'user_rate_limit' in kwargs
assert 'channel_rate_limit' in kwargs
assert 'global_rate_limit' in kwargs
assert 'user_rate_message' in kwargs
assert 'channel_rate_message' in kwargs
assert 'global_rate_message' in kwargs
assert 'default_rate_message' in kwargs
assert kwargs['rate_limit_admins'] is True
assert kwargs['user_rate_limit'] == 20
assert kwargs['channel_rate_limit'] == 0
assert kwargs['global_rate_limit'] == 0
Expand All @@ -1303,21 +1321,23 @@ def handler(wrapped, trigger):
def test_kwargs_from_callable_rate_limit_channel(mockbot):
# prepare callable
@plugin.rule(r'hello', r'hi', r'hey', r'hello|hi')
@plugin.rate_channel(20, 'Channel message.')
@plugin.rate_channel(20, 'Channel message.', True)
def handler(wrapped, trigger):
wrapped.reply('Hi!')

loader.clean_callable(handler, mockbot.settings)

# get kwargs
kwargs = rules.Rule.kwargs_from_callable(handler)
assert 'rate_limit_admins' in kwargs
assert 'user_rate_limit' in kwargs
assert 'channel_rate_limit' in kwargs
assert 'global_rate_limit' in kwargs
assert 'user_rate_message' in kwargs
assert 'channel_rate_message' in kwargs
assert 'global_rate_message' in kwargs
assert 'default_rate_message' in kwargs
assert kwargs['rate_limit_admins'] is True
assert kwargs['user_rate_limit'] == 0
assert kwargs['channel_rate_limit'] == 20
assert kwargs['global_rate_limit'] == 0
Expand All @@ -1330,21 +1350,23 @@ def handler(wrapped, trigger):
def test_kwargs_from_callable_rate_limit_server(mockbot):
# prepare callable
@plugin.rule(r'hello', r'hi', r'hey', r'hello|hi')
@plugin.rate_global(20, 'Server message.')
@plugin.rate_global(20, 'Server message.', True)
def handler(wrapped, trigger):
wrapped.reply('Hi!')

loader.clean_callable(handler, mockbot.settings)

# get kwargs
kwargs = rules.Rule.kwargs_from_callable(handler)
assert 'rate_limit_admins' in kwargs
assert 'user_rate_limit' in kwargs
assert 'channel_rate_limit' in kwargs
assert 'global_rate_limit' in kwargs
assert 'user_rate_message' in kwargs
assert 'channel_rate_message' in kwargs
assert 'global_rate_message' in kwargs
assert 'default_rate_message' in kwargs
assert kwargs['rate_limit_admins'] is True
assert kwargs['user_rate_limit'] == 0
assert kwargs['channel_rate_limit'] == 0
assert kwargs['global_rate_limit'] == 20
Expand Down