Skip to content

Commit

Permalink
loader, plugin, plugins.rules: add allow_bots decorator
Browse files Browse the repository at this point in the history
Noted some gaps in the test suite regarding the loader setting defaults
for certain callable attributes (including this new one), but elected to
leave that for a future patch.
  • Loading branch information
dgw committed Jan 26, 2022
1 parent 3db0992 commit 2f27ab4
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 2 deletions.
1 change: 1 addition & 0 deletions sopel/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def clean_callable(func, config):
# confusing to other code (and a waste of memory) for jobs.
return

func.allow_bots = getattr(func, 'allow_bots', False)
func.echo = getattr(func, 'echo', False)
func.priority = getattr(func, 'priority', 'medium')
func.output_prefix = getattr(func, 'output_prefix', '')
Expand Down
23 changes: 23 additions & 0 deletions sopel/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
# decorators
'action_command',
'action_commands',
'allow_bots',
'command',
'commands',
'ctcp',
Expand Down Expand Up @@ -463,6 +464,28 @@ def add_attribute(function):
return add_attribute


def allow_bots(
function: typing.Optional[typing.Any] = None,
) -> typing.Union[typing.Any, typing.Callable]:
"""Decorate a function to specify that it should receive events from bots.
On networks implementing the `Bot Mode specification`__, messages and
other events from other clients that have identified themselves as bots
will be tagged as such, and Sopel will ignore them by default. This
decorator allow opting in for a function to receive these events anyway.
.. __: https://ircv3.net/specs/extensions/bot-mode
"""
def add_attribute(function):
function.allow_bots = True
return function

# hack to allow both @allow_bots and @allow_bots() to work
if callable(function):
return add_attribute(function)
return add_attribute


def echo(
function: typing.Optional[typing.Any] = None,
) -> typing.Union[typing.Any, typing.Callable]:
Expand Down
29 changes: 27 additions & 2 deletions sopel/plugins/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,23 @@ def match_intent(self, intent) -> bool:
:rtype: bool
"""

@abc.abstractmethod
def allow_bots(self) -> bool:
"""Tell if the rule should match bot commands.
:return: ``True`` when the rule allows bot commands,
``False`` otherwise
A "bot command" is any IRC protocol command or numeric that has been
tagged as ``bot`` (or ``draft/bot``) by the IRC server.
.. seealso::
The `IRCv3 Bot Mode specification`__.
.. __: https://ircv3.net/specs/extensions/bot-mode
"""

@abc.abstractmethod
def allow_echo(self) -> bool:
"""Tell if the rule should match echo messages.
Expand Down Expand Up @@ -758,6 +775,7 @@ def kwargs_from_callable(cls, handler):
'priority': getattr(handler, 'priority', PRIORITY_MEDIUM),
'events': getattr(handler, 'event', []),
'intents': getattr(handler, 'intents', []),
'allow_bots': getattr(handler, 'allow_bots', False),
'allow_echo': getattr(handler, 'echo', False),
'threaded': getattr(handler, 'thread', True),
'output_prefix': getattr(handler, 'output_prefix', ''),
Expand Down Expand Up @@ -847,6 +865,7 @@ def __init__(self,
handler=None,
events=None,
intents=None,
allow_bots=False,
allow_echo=False,
threaded=True,
output_prefix=None,
Expand All @@ -867,6 +886,7 @@ def __init__(self,
# filters
self._events = events or ['PRIVMSG']
self._intents = intents or []
self._allow_bots = bool(allow_bots)
self._allow_echo = bool(allow_echo)

# execution
Expand Down Expand Up @@ -976,8 +996,10 @@ def match_preconditions(self, bot, pretrigger):
return (
self.match_event(event) and
self.match_intent(intent) and
(not is_bot_message or (is_echo_message and self.allow_echo())) and
(not is_echo_message or self.allow_echo())
(
(not is_bot_message or self.allow_bots()) or
(is_echo_message and self.allow_echo())
) and (not is_echo_message or self.allow_echo())
)

def parse(self, text):
Expand All @@ -998,6 +1020,9 @@ def match_intent(self, intent):
for regex in self._intents
))

def allow_bots(self):
return self._allow_bots

def allow_echo(self):
return self._allow_echo

Expand Down
39 changes: 39 additions & 0 deletions test/plugins/test_plugins_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,45 @@ def test_rule_match_privmsg_echo(mockbot):
assert match.group(0) == 'Hi!'


@pytest.mark.parametrize(
'is_bot, allow_bots, is_echo, allow_echo, should_match',
[
(True, True, True, True, True),
(True, True, True, False, False),
(True, True, False, True, True),
(True, True, False, False, True),
(True, False, True, True, True),
(True, False, True, False, False),
(True, False, False, True, False),
(True, False, False, False, False),
(False, True, True, True, True),
(False, True, True, False, False),
(False, True, False, True, True),
(False, True, False, False, True),
(False, False, True, True, True),
(False, False, True, False, False),
(False, False, False, True, True),
(False, False, False, False, True),
])
def test_rule_match_privmsg_echo_and_bot_tag(
is_bot, allow_bots, is_echo, allow_echo, should_match, mockbot
):
line = '{tags}:{nick}[email protected] PRIVMSG #sopel :Hi!'.format(
tags='@bot ' if is_bot else '',
nick=mockbot.nick if is_echo else 'SomeUser',
)
pretrigger = trigger.PreTrigger(mockbot.nick, line)
regex = re.compile(r'.*')

rule = rules.Rule([regex], allow_bots=allow_bots, allow_echo=allow_echo)
matches = list(rule.match(mockbot, pretrigger))

if should_match:
assert len(matches) == 1, 'This combination should match the Rule'
else:
assert not matches, 'This combination should not match the Rule'


def test_rule_match_join(mockbot):
line = ':[email protected] JOIN #sopel'
pretrigger = trigger.PreTrigger(mockbot.nick, line)
Expand Down
21 changes: 21 additions & 0 deletions test/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@
"""


def test_allow_bots():
# test decorator with parentheses
@plugin.allow_bots()
def mock(bot, trigger, match):
return True
assert mock.allow_bots is True

# test decorator without parentheses
@plugin.allow_bots
def mock(bot, trigger, match):
return True
assert mock.allow_bots is True

# test without decorator
def mock(bot, trigger, match):
return True
# on undecorated callables, the attr only exists after the loader loads them
# so this cannot `assert mock.allow_bots is False` here
assert not hasattr(mock, 'allow_bots')


def test_find():
@plugin.find('.*')
def mock(bot, trigger, match):
Expand Down

0 comments on commit 2f27ab4

Please sign in to comment.