Skip to content

Commit 94e2c92

Browse files
Exireldgw
andcommitted
bot, rules: fix rate limiting rules without a rate limit
Co-authored-by: dgw <[email protected]>
1 parent 0e5c976 commit 94e2c92

File tree

4 files changed

+144
-77
lines changed

4 files changed

+144
-77
lines changed

sopel/bot.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -602,30 +602,26 @@ def rate_limit_info(
602602
if trigger.admin or rule.is_unblockable():
603603
return False, None
604604

605+
nick = trigger.nick
605606
is_channel = trigger.sender and not trigger.sender.is_nick()
606607
channel = trigger.sender if is_channel else None
607608

608609
at_time = trigger.time
609-
610-
user_metrics = rule.get_user_metrics(trigger.nick)
611-
channel_metrics = rule.get_channel_metrics(channel)
612-
global_metrics = rule.get_global_metrics()
613-
614-
if user_metrics.is_limited(at_time - rule.user_rate_limit):
610+
if rule.is_user_rate_limited(nick, at_time):
615611
template = rule.user_rate_template
616612
rate_limit_type = "user"
617613
rate_limit = rule.user_rate_limit
618-
metrics = user_metrics
619-
elif is_channel and channel_metrics.is_limited(at_time - rule.channel_rate_limit):
614+
metrics = rule.get_user_metrics(nick)
615+
elif channel and rule.is_channel_rate_limited(channel, at_time):
620616
template = rule.channel_rate_template
621617
rate_limit_type = "channel"
622618
rate_limit = rule.channel_rate_limit
623-
metrics = channel_metrics
624-
elif global_metrics.is_limited(at_time - rule.global_rate_limit):
619+
metrics = rule.get_channel_metrics(channel)
620+
elif rule.is_global_rate_limited(at_time):
625621
template = rule.global_rate_template
626622
rate_limit_type = "global"
627623
rate_limit = rule.global_rate_limit
628-
metrics = global_metrics
624+
metrics = rule.get_global_metrics()
629625
else:
630626
return False, None
631627

sopel/plugins/rules.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -765,40 +765,49 @@ def global_rate_limit(self) -> datetime.timedelta:
765765
def is_user_rate_limited(
766766
self,
767767
nick: Identifier,
768-
at_time: Optional[datetime.datetime] = None,
768+
at_time: datetime.datetime,
769769
) -> bool:
770770
"""Tell when the rule reached the ``nick``'s rate limit.
771771
772772
:param nick: the nick associated with this check
773-
:param at_time: optional aware datetime for the rate limit check;
774-
if not given, ``utcnow`` will be used
773+
:param at_time: aware datetime for the rate limit check
775774
:return: ``True`` when the rule reached the limit, ``False`` otherwise.
775+
776+
.. versionchanged:: 8.0.1
777+
778+
Parameter ``at_time`` is now required.
779+
776780
"""
777781

778782
@abc.abstractmethod
779783
def is_channel_rate_limited(
780784
self,
781785
channel: Identifier,
782-
at_time: Optional[datetime.datetime] = None,
786+
at_time: datetime.datetime,
783787
) -> bool:
784788
"""Tell when the rule reached the ``channel``'s rate limit.
785789
786790
:param channel: the channel associated with this check
787-
:param at_time: optional aware datetime for the rate limit check;
788-
if not given, ``utcnow`` will be used
791+
:param at_time: aware datetime for the rate limit check
789792
:return: ``True`` when the rule reached the limit, ``False`` otherwise.
793+
794+
.. versionchanged:: 8.0.1
795+
796+
Parameter ``at_time`` is now required.
797+
790798
"""
791799

792800
@abc.abstractmethod
793-
def is_global_rate_limited(
794-
self,
795-
at_time: Optional[datetime.datetime] = None,
796-
) -> bool:
801+
def is_global_rate_limited(self, at_time: datetime.datetime) -> bool:
797802
"""Tell when the rule reached the global rate limit.
798803
799-
:param at_time: optional aware datetime for the rate limit check;
800-
if not given, ``utcnow`` will be used
804+
:param at_time: aware datetime for the rate limit check
801805
:return: ``True`` when the rule reached the limit, ``False`` otherwise.
806+
807+
.. versionchanged:: 8.0.1
808+
809+
Parameter ``at_time`` is now required.
810+
802811
"""
803812

804813
@property
@@ -1209,29 +1218,29 @@ def global_rate_limit(self) -> datetime.timedelta:
12091218
def is_user_rate_limited(
12101219
self,
12111220
nick: Identifier,
1212-
at_time: Optional[datetime.datetime] = None,
1221+
at_time: datetime.datetime,
12131222
) -> bool:
1214-
if at_time is None:
1215-
at_time = datetime.datetime.now(datetime.timezone.utc)
1223+
if self._user_rate_limit <= 0:
1224+
return False
1225+
12161226
metrics = self.get_user_metrics(nick)
12171227
return metrics.is_limited(at_time - self.user_rate_limit)
12181228

12191229
def is_channel_rate_limited(
12201230
self,
12211231
channel: Identifier,
1222-
at_time: Optional[datetime.datetime] = None,
1232+
at_time: datetime.datetime,
12231233
) -> bool:
1224-
if at_time is None:
1225-
at_time = datetime.datetime.now(datetime.timezone.utc)
1234+
if self._channel_rate_limit <= 0:
1235+
return False
1236+
12261237
metrics = self.get_channel_metrics(channel)
12271238
return metrics.is_limited(at_time - self.channel_rate_limit)
12281239

1229-
def is_global_rate_limited(
1230-
self,
1231-
at_time: Optional[datetime.datetime] = None,
1232-
) -> bool:
1233-
if at_time is None:
1234-
at_time = datetime.datetime.now(datetime.timezone.utc)
1240+
def is_global_rate_limited(self, at_time: datetime.datetime) -> bool:
1241+
if self._global_rate_limit <= 0:
1242+
return False
1243+
12351244
metrics = self.get_global_metrics()
12361245
return metrics.is_limited(at_time - self.global_rate_limit)
12371246

test/plugins/test_plugins_rules.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,14 +1566,16 @@ def handler(bot, trigger):
15661566
global_rate_limit=20,
15671567
channel_rate_limit=20,
15681568
)
1569-
assert rule.is_user_rate_limited(mocktrigger.nick) is False
1570-
assert rule.is_channel_rate_limited(mocktrigger.sender) is False
1571-
assert rule.is_global_rate_limited() is False
1569+
at_time = datetime.datetime.now(datetime.timezone.utc)
1570+
assert rule.is_user_rate_limited(mocktrigger.nick, at_time) is False
1571+
assert rule.is_channel_rate_limited(mocktrigger.sender, at_time) is False
1572+
assert rule.is_global_rate_limited(at_time) is False
15721573

15731574
rule.execute(mockbot, mocktrigger)
1574-
assert rule.is_user_rate_limited(mocktrigger.nick) is True
1575-
assert rule.is_channel_rate_limited(mocktrigger.sender) is True
1576-
assert rule.is_global_rate_limited() is True
1575+
at_time = datetime.datetime.now(datetime.timezone.utc)
1576+
assert rule.is_user_rate_limited(mocktrigger.nick, at_time) is True
1577+
assert rule.is_channel_rate_limited(mocktrigger.sender, at_time) is True
1578+
assert rule.is_global_rate_limited(at_time) is True
15771579

15781580

15791581
def test_rule_rate_limit_no_limit(mockbot, triggerfactory):
@@ -1592,14 +1594,16 @@ def handler(bot, trigger):
15921594
global_rate_limit=0,
15931595
channel_rate_limit=0,
15941596
)
1595-
assert rule.is_user_rate_limited(mocktrigger.nick) is False
1596-
assert rule.is_channel_rate_limited(mocktrigger.sender) is False
1597-
assert rule.is_global_rate_limited() is False
1597+
at_time = datetime.datetime.now(datetime.timezone.utc)
1598+
assert rule.is_user_rate_limited(mocktrigger.nick, at_time) is False
1599+
assert rule.is_channel_rate_limited(mocktrigger.sender, at_time) is False
1600+
assert rule.is_global_rate_limited(at_time) is False
15981601

15991602
rule.execute(mockbot, mocktrigger)
1600-
assert rule.is_user_rate_limited(mocktrigger.nick) is False
1601-
assert rule.is_channel_rate_limited(mocktrigger.sender) is False
1602-
assert rule.is_global_rate_limited() is False
1603+
at_time = datetime.datetime.now(datetime.timezone.utc)
1604+
assert rule.is_user_rate_limited(mocktrigger.nick, at_time) is False
1605+
assert rule.is_channel_rate_limited(mocktrigger.sender, at_time) is False
1606+
assert rule.is_global_rate_limited(at_time) is False
16031607

16041608

16051609
def test_rule_rate_limit_ignore_rate_limit(mockbot, triggerfactory):
@@ -1619,14 +1623,16 @@ def handler(bot, trigger):
16191623
channel_rate_limit=20,
16201624
threaded=False, # make sure there is no race-condition here
16211625
)
1622-
assert rule.is_user_rate_limited(mocktrigger.nick) is False
1623-
assert rule.is_channel_rate_limited(mocktrigger.sender) is False
1624-
assert rule.is_global_rate_limited() is False
1626+
at_time = datetime.datetime.now(datetime.timezone.utc)
1627+
assert rule.is_user_rate_limited(mocktrigger.nick, at_time) is False
1628+
assert rule.is_channel_rate_limited(mocktrigger.sender, at_time) is False
1629+
assert rule.is_global_rate_limited(at_time) is False
16251630

16261631
rule.execute(mockbot, mocktrigger)
1627-
assert rule.is_user_rate_limited(mocktrigger.nick) is False
1628-
assert rule.is_channel_rate_limited(mocktrigger.sender) is False
1629-
assert rule.is_global_rate_limited() is False
1632+
at_time = datetime.datetime.now(datetime.timezone.utc)
1633+
assert rule.is_user_rate_limited(mocktrigger.nick, at_time) is False
1634+
assert rule.is_channel_rate_limited(mocktrigger.sender, at_time) is False
1635+
assert rule.is_global_rate_limited(at_time) is False
16301636

16311637

16321638
def test_rule_rate_limit_messages(mockbot, triggerfactory):

test/test_bot.py

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
if typing.TYPE_CHECKING:
1717
from sopel.config import Config
18-
from sopel.tests.factories import BotFactory, IRCFactory, UserFactory
18+
from sopel.tests.factories import (
19+
BotFactory, ConfigFactory, IRCFactory, TriggerFactory, UserFactory,
20+
)
1921
from sopel.tests.mocks import MockIRCServer
2022

2123

@@ -81,17 +83,17 @@ def ignored():
8183

8284

8385
@pytest.fixture
84-
def tmpconfig(configfactory):
86+
def tmpconfig(configfactory: ConfigFactory) -> Config:
8587
return configfactory('test.cfg', TMP_CONFIG)
8688

8789

8890
@pytest.fixture
89-
def mockbot(tmpconfig, botfactory):
91+
def mockbot(tmpconfig: Config, botfactory: BotFactory) -> bot.Sopel:
9092
return botfactory(tmpconfig)
9193

9294

9395
@pytest.fixture
94-
def mockplugin(tmpdir):
96+
def mockplugin(tmpdir) -> plugins.handlers.PyFilePlugin:
9597
root = tmpdir.mkdir('loader_mods')
9698
mod_file = root.join('mockplugin.py')
9799
mod_file.write(MOCK_MODULE_CONTENT)
@@ -676,7 +678,7 @@ def url_callback_http(bot, trigger, match):
676678
# call_rule
677679

678680
@pytest.fixture
679-
def match_hello_rule(mockbot, triggerfactory):
681+
def match_hello_rule(mockbot: bot.Sopel, triggerfactory: TriggerFactory):
680682
"""Helper for generating matches to each `Rule` in the following tests"""
681683
def _factory(rule_hello):
682684
# trigger
@@ -694,7 +696,25 @@ def _factory(rule_hello):
694696
return _factory
695697

696698

697-
def test_call_rule(mockbot, match_hello_rule):
699+
@pytest.fixture
700+
def multimatch_hello_rule(mockbot: bot.Sopel, triggerfactory: TriggerFactory):
701+
def _factory(rule_hello):
702+
# trigger
703+
line = ':[email protected] PRIVMSG #channel :hello hello hello'
704+
705+
trigger = triggerfactory(mockbot, line)
706+
pretrigger = trigger._pretrigger
707+
708+
for match in rule_hello.match(mockbot, pretrigger):
709+
wrapper = bot.SopelWrapper(mockbot, trigger)
710+
yield match, trigger, wrapper
711+
return _factory
712+
713+
714+
def test_call_rule(
715+
mockbot: bot.Sopel,
716+
match_hello_rule: typing.Callable,
717+
) -> None:
698718
# setup
699719
items = []
700720

@@ -721,9 +741,10 @@ def testrule(bot, trigger):
721741
assert items == [1]
722742

723743
# assert the rule is not rate limited
724-
assert not rule_hello.is_user_rate_limited(Identifier('Test'))
725-
assert not rule_hello.is_channel_rate_limited('#channel')
726-
assert not rule_hello.is_global_rate_limited()
744+
at_time = datetime.now(timezone.utc)
745+
assert not rule_hello.is_user_rate_limited(Identifier('Test'), at_time)
746+
assert not rule_hello.is_channel_rate_limited('#channel', at_time)
747+
assert not rule_hello.is_global_rate_limited(at_time)
727748

728749
match, rule_trigger, wrapper = match_hello_rule(rule_hello)
729750

@@ -738,6 +759,36 @@ def testrule(bot, trigger):
738759
assert items == [1, 1]
739760

740761

762+
def test_call_rule_multiple_matches(
763+
mockbot: bot.Sopel,
764+
multimatch_hello_rule: typing.Callable,
765+
) -> None:
766+
# setup
767+
items = []
768+
769+
def testrule(bot, trigger):
770+
bot.say('hi')
771+
items.append(1)
772+
return "Return Value"
773+
774+
find_hello = rules.FindRule(
775+
[re.compile(r'(hi|hello|hey|sup)')],
776+
plugin='testplugin',
777+
label='testrule',
778+
handler=testrule)
779+
780+
for match, rule_trigger, wrapper in multimatch_hello_rule(find_hello):
781+
mockbot.call_rule(find_hello, wrapper, rule_trigger)
782+
783+
# assert the rule has been executed three times now
784+
assert mockbot.backend.message_sent == rawlist(
785+
'PRIVMSG #channel :hi',
786+
'PRIVMSG #channel :hi',
787+
'PRIVMSG #channel :hi',
788+
)
789+
assert items == [1, 1, 1]
790+
791+
741792
def test_call_rule_rate_limited_user(mockbot, match_hello_rule):
742793
items = []
743794

@@ -767,9 +818,10 @@ def testrule(bot, trigger):
767818
assert items == [1]
768819

769820
# assert the rule is now rate limited
770-
assert rule_hello.is_user_rate_limited(Identifier('Test'))
771-
assert not rule_hello.is_channel_rate_limited('#channel')
772-
assert not rule_hello.is_global_rate_limited()
821+
at_time = datetime.now(timezone.utc)
822+
assert rule_hello.is_user_rate_limited(Identifier('Test'), at_time)
823+
assert not rule_hello.is_channel_rate_limited('#channel', at_time)
824+
assert not rule_hello.is_global_rate_limited(at_time)
773825

774826
match, rule_trigger, wrapper = match_hello_rule(rule_hello)
775827

@@ -852,9 +904,10 @@ def testrule(bot, trigger):
852904
assert items == [1]
853905

854906
# assert the rule is now rate limited
855-
assert not rule_hello.is_user_rate_limited(Identifier('Test'))
856-
assert rule_hello.is_channel_rate_limited('#channel')
857-
assert not rule_hello.is_global_rate_limited()
907+
at_time = datetime.now(timezone.utc)
908+
assert not rule_hello.is_user_rate_limited(Identifier('Test'), at_time)
909+
assert rule_hello.is_channel_rate_limited('#channel', at_time)
910+
assert not rule_hello.is_global_rate_limited(at_time)
858911

859912
match, rule_trigger, wrapper = match_hello_rule(rule_hello)
860913

@@ -897,9 +950,10 @@ def testrule(bot, trigger):
897950
assert items == [1]
898951

899952
# assert the rule is now rate limited
900-
assert not rule_hello.is_user_rate_limited(Identifier('Test'))
901-
assert rule_hello.is_channel_rate_limited('#channel')
902-
assert not rule_hello.is_global_rate_limited()
953+
at_time = datetime.now(timezone.utc)
954+
assert not rule_hello.is_user_rate_limited(Identifier('Test'), at_time)
955+
assert rule_hello.is_channel_rate_limited('#channel', at_time)
956+
assert not rule_hello.is_global_rate_limited(at_time)
903957

904958
match, rule_trigger, wrapper = match_hello_rule(rule_hello)
905959

@@ -942,9 +996,10 @@ def testrule(bot, trigger):
942996
assert items == [1]
943997

944998
# assert the rule is now rate limited
945-
assert not rule_hello.is_user_rate_limited(Identifier('Test'))
946-
assert not rule_hello.is_channel_rate_limited('#channel')
947-
assert rule_hello.is_global_rate_limited()
999+
at_time = datetime.now(timezone.utc)
1000+
assert not rule_hello.is_user_rate_limited(Identifier('Test'), at_time)
1001+
assert not rule_hello.is_channel_rate_limited('#channel', at_time)
1002+
assert rule_hello.is_global_rate_limited(at_time)
9481003

9491004
match, rule_trigger, wrapper = match_hello_rule(rule_hello)
9501005

@@ -987,9 +1042,10 @@ def testrule(bot, trigger):
9871042
assert items == [1]
9881043

9891044
# assert the rule is now rate limited
990-
assert not rule_hello.is_user_rate_limited(Identifier('Test'))
991-
assert not rule_hello.is_channel_rate_limited('#channel')
992-
assert rule_hello.is_global_rate_limited()
1045+
at_time = datetime.now(timezone.utc)
1046+
assert not rule_hello.is_user_rate_limited(Identifier('Test'), at_time)
1047+
assert not rule_hello.is_channel_rate_limited('#channel', at_time)
1048+
assert rule_hello.is_global_rate_limited(at_time)
9931049

9941050
match, rule_trigger, wrapper = match_hello_rule(rule_hello)
9951051

0 commit comments

Comments
 (0)