Skip to content

Commit

Permalink
Merge pull request #2434 from SnoopJ/feature/gh2325-rework-rate-limit
Browse files Browse the repository at this point in the history
Add more placeholders to rate-limit messages, move template interpolation from `Rule` to `Sopel`
  • Loading branch information
dgw authored Jun 2, 2023
2 parents d84ffb2 + 9873465 commit 1538ec8
Show file tree
Hide file tree
Showing 5 changed files with 377 additions and 225 deletions.
84 changes: 63 additions & 21 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from __future__ import annotations

from ast import literal_eval
from datetime import datetime
import datetime
import inspect
import itertools
import logging
Expand Down Expand Up @@ -592,6 +592,62 @@ def register_urls(self, urls: Iterable) -> None:
except plugins.exceptions.PluginError as err:
LOGGER.error("Cannot register URL callback: %s", err)

def rate_limit_info(
self,
rule: plugin_rules.Rule,
trigger: Trigger,
) -> Tuple[bool, Optional[str]]:
if trigger.admin or rule.is_unblockable():
return False, None

is_channel = trigger.sender and not trigger.sender.is_nick()
channel = trigger.sender if is_channel else None

at_time = trigger.time

user_metrics = rule.get_user_metrics(trigger.nick)
channel_metrics = rule.get_channel_metrics(channel)
global_metrics = rule.get_global_metrics()

if user_metrics.is_limited(at_time - rule.user_rate_limit):
template = rule.user_rate_template
rate_limit_type = "user"
rate_limit = rule.user_rate_limit
metrics = user_metrics
elif is_channel and channel_metrics.is_limited(at_time - rule.channel_rate_limit):
template = rule.channel_rate_template
rate_limit_type = "channel"
rate_limit = rule.channel_rate_limit
metrics = channel_metrics
elif global_metrics.is_limited(at_time - rule.global_rate_limit):
template = rule.global_rate_template
rate_limit_type = "global"
rate_limit = rule.global_rate_limit
metrics = global_metrics
else:
return False, None

next_time = metrics.last_time + rate_limit
time_left = next_time - at_time

message: Optional[str] = None

if template:
message = template.format(
nick=trigger.nick,
channel=channel or 'private message',
sender=trigger.sender,
plugin=rule.get_plugin_name(),
label=rule.get_rule_label(),
time_left=time_left,
time_left_sec=time_left.total_seconds(),
rate_limit=rate_limit,
rate_limit_sec=rate_limit.total_seconds(),
rate_limit_type=rate_limit_type,
)

return True, message

# message dispatch

def call_rule(
Expand All @@ -604,25 +660,11 @@ def call_rule(
context = trigger.sender
is_channel = context and not context.is_nick()

# rate limiting
if not trigger.admin and not rule.is_unblockable():
if rule.is_user_rate_limited(nick):
message = rule.get_user_rate_message(nick)
if message:
sopel.notice(message, destination=nick)
return

if is_channel and rule.is_channel_rate_limited(context):
message = rule.get_channel_rate_message(nick, context)
if message:
sopel.notice(message, destination=nick)
return

if rule.is_global_rate_limited():
message = rule.get_global_rate_message(nick)
if message:
sopel.notice(message, destination=nick)
return
limited, limit_msg = self.rate_limit_info(rule, trigger)
if limit_msg:
sopel.notice(limit_msg, destination=nick)
if limited:
return

# channel config
if is_channel and context in self.config:
Expand Down Expand Up @@ -1003,7 +1045,7 @@ def error(

if trigger:
message = '{} from {} at {}. Message was: {}'.format(
message, trigger.nick, str(datetime.utcnow()), trigger.group(0)
message, trigger.nick, str(datetime.datetime.utcnow()), trigger.group(0)
)

LOGGER.exception(message)
Expand Down
43 changes: 29 additions & 14 deletions sopel/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,10 +1112,27 @@ def rate(
@rate(10, 10, 10, message='Hit the rate limit for this function.')
# will send a NOTICE
The message can contain a placeholder for ``nick``: the message will be
formatted with the nick that hit rate limit::
@rate(10, 10, 2, message='Sorry {nick}, you hit the rate limit!')
The message can contain placeholders which will be filled in:
* ``nick``: the nick that hit the rate limit
* ``channel``: the channel in which the rate limit was hit (will be
``'private-message'`` for private messages)
* ``sender``: the sender (nick or channel) of the message which hit
the rate limit
* ``plugin``: the name of the plugin that hit the rate limit
* ``label``: the label of the plugin handler that hit the rate limit
* ``time_left``: the time remaining before the rate limit expires, as
a string
* ``time_left_sec``: the time remaining before the rate limit expires,
expressed in number of seconds
* ``rate_limit``: the rate limit, as a string
* ``rate_limit_sec``: the rate limit, expressed in number of seconds
* ``rate_limit_type``: the type of rate limit that was hit (one of
``user, group, global``)
For example::
@rate(10, 10, 2, message='Sorry {nick}, you hit the {rate_limit_type} rate limit!')
Rate-limited functions that use scheduled future commands should import
:class:`threading.Timer` instead of :mod:`sched`, or rate limiting will
Expand Down Expand Up @@ -1168,10 +1185,9 @@ def rate_user(
# will send a NOTICE only when a user hits their own limit
# as other rate limits don't have any message set
The message can contain a placeholder for ``nick``: the message will be
formatted with the nick that hit rate limit::
The message can contain the same placeholders supported by :func:`rate`::
@rate_user(5, 'Sorry {nick}, you hit your 5s limit!')
@rate_user(5, 'Sorry {nick}, you hit your {rate_limit_sec}s limit!')
If you don't provide a message, the default message set by :func:`rate`
(if any) will be used instead.
Expand Down Expand Up @@ -1213,13 +1229,11 @@ def rate_channel(
If you don't provide a message, the default message set by :func:`rate`
(if any) will be used instead.
The message can contain placeholders for ``nick`` and ``channel``: the
message will be formatted with the nick that hit rate limit for said
``channel``::
The message can contain the same placeholders supported by :func:`rate`::
@rate_channel(
5,
'Sorry {nick}, you hit the 5s limit for the {channel} channel!',
'Sorry {nick}, you hit the {rate_limit_sec}s limit for the {channel} channel!',
)
.. versionadded:: 8.0
Expand Down Expand Up @@ -1248,7 +1262,9 @@ def rate_global(
:param message: optional; message sent as NOTICE when a user hits the limit
This decorator can be used alone or with the :func:`rate` decorator, as it
will always take precedence::
will always take precedence.
For example::
@rate(10, 10, 10)
@rate_global(5, 'You hit the global rate limit for this function.')
Expand All @@ -1259,8 +1275,7 @@ def rate_global(
If you don't provide a message, the default message set by :func:`rate`
(if any) will be used instead.
The message can contain a placeholder for ``nick``: the message will be
formatted with the nick that hit rate limit::
The message can contain the same placeholders supported by :func:`rate`::
@rate_global(5, 'Sorry {nick}, you hit the 5s limit!')
Expand Down
Loading

0 comments on commit 1538ec8

Please sign in to comment.