From 59b5bb6d5bb1e6a7a2a80feb9146b864e2ad8522 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 9 Apr 2023 00:04:22 +0200 Subject: [PATCH] bot, trigger: use status prefix in SopelWrapper With various minor fixes thanks to my reviewers. Co-authored-by: James Gerity Co-authored-by: dgw --- sopel/bot.py | 52 +++++++++++++++++++++++++++--- sopel/trigger.py | 13 ++++++++ test/test_bot.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/sopel/bot.py b/sopel/bot.py index 5354602b14..64d56cd546 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -1256,6 +1256,35 @@ def __getattr__(self, attr): def __setattr__(self, attr, value): return setattr(self._bot, attr, value) + @property + def default_destination(self) -> Optional[str]: + """Default say/reply destination for the associated Trigger. + + :return: the channel (with status prefix) or nick to send messages to + + This property returns the :class:`str` version of the destination that + will be used by default by these methods: + + * :meth:`say` + * :meth:`reply` + * :meth:`action` + * :meth:`notice` + + For a channel, it also ensures that the status-specific prefix is added + to the result, so the bot replies with the same status. + """ + if not self._trigger.sender: + return None + + # ensure str and not Identifier + destination = str(self._trigger.sender) + + # prepend status prefix if it exists + if self._trigger.status_prefix: + destination = self._trigger.status_prefix + destination + + return destination + def say(self, message, destination=None, max_messages=1, truncation='', trailing=''): """Override ``Sopel.say`` to use trigger source by default. @@ -1280,8 +1309,15 @@ def say(self, message, destination=None, max_messages=1, truncation='', trailing """ if destination is None: - destination = self._trigger.sender - self._bot.say(self._out_pfx + message, destination, max_messages, truncation, trailing) + destination = self.default_destination + + self._bot.say( + self._out_pfx + message, + destination, + max_messages, + truncation, + trailing, + ) def action(self, message, destination=None): """Override ``Sopel.action`` to use trigger source by default. @@ -1298,7 +1334,8 @@ def action(self, message, destination=None): :meth:`sopel.bot.Sopel.action` """ if destination is None: - destination = self._trigger.sender + destination = self.default_destination + self._bot.action(message, destination) def notice(self, message, destination=None): @@ -1316,7 +1353,8 @@ def notice(self, message, destination=None): :meth:`sopel.bot.Sopel.notice` """ if destination is None: - destination = self._trigger.sender + destination = self.default_destination + self._bot.notice(self._out_pfx + message, destination) def reply(self, message, destination=None, reply_to=None, notice=False): @@ -1339,9 +1377,11 @@ def reply(self, message, destination=None, reply_to=None, notice=False): :meth:`sopel.bot.Sopel.reply` """ if destination is None: - destination = self._trigger.sender + destination = self.default_destination + if reply_to is None: reply_to = self._trigger.nick + self._bot.reply(message, destination, reply_to, notice) def kick(self, nick, channel=None, message=None): @@ -1364,6 +1404,8 @@ def kick(self, nick, channel=None, message=None): raise RuntimeError('Error: KICK requires a channel.') else: channel = self._trigger.sender + if nick is None: raise RuntimeError('Error: KICK requires a nick.') + self._bot.kick(nick, channel, message) diff --git a/sopel/trigger.py b/sopel/trigger.py index 00866e37cf..6c509e1c97 100644 --- a/sopel/trigger.py +++ b/sopel/trigger.py @@ -329,6 +329,18 @@ class Trigger(str): else: # message sent from a channel + .. important:: + + If the message was sent to a `specific status prefix`__, the ``sender`` + does not include the status prefix. Be sure to use the + :attr:`status_prefix` when replying. + + Note that the ``bot`` argument passed to plugin callables is a + :class:`~sopel.bot.SopelWrapper` that handles this for the default + ``destination`` of the methods it overrides (most importantly, + :meth:`~sopel.bot.SopelWrapper.say` & + :meth:`~sopel.bot.SopelWrapper.reply`). + .. warning:: The ``sender`` Will be ``None`` for commands that have no implicit @@ -337,6 +349,7 @@ class Trigger(str): The :attr:`COMMANDS_WITH_CONTEXT` attribute lists IRC commands for which ``sender`` can be relied upon. + .. __: https://modern.ircdocs.horse/#statusmsg-parameter """ status_prefix = property(lambda self: self._pretrigger.status_prefix) """The prefix used for the :attr:`sender` for status-specific messages. diff --git a/test/test_bot.py b/test/test_bot.py index 8530bd82a7..fe3bb770cf 100644 --- a/test/test_bot.py +++ b/test/test_bot.py @@ -102,6 +102,32 @@ def mockplugin(tmpdir): # ----------------------------------------------------------------------------- # sopel.bot.SopelWrapper +def test_wrapper_default_destination(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( + mockbot, ':Test!test@example.com PRIVMSG #channel :test message') + + assert wrapper.default_destination == '#channel' + + +def test_wrapper_default_destination_none(mockbot, triggerfactory): + wrapper = triggerfactory.wrapper( + mockbot, ':irc.example.com 301 Sopel :I am away.') + + assert wrapper.default_destination is None + + +def test_wrapper_default_destination_statusmsg(mockbot, triggerfactory): + mockbot._isupport = mockbot.isupport.apply( + STATUSMSG=tuple('+'), + ) + + wrapper = triggerfactory.wrapper( + mockbot, ':Test!test@example.com PRIVMSG +#channel :test message') + + assert wrapper._trigger.sender == '#channel' + assert wrapper.default_destination == '+#channel' + + def test_wrapper_say(mockbot, triggerfactory): wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') @@ -112,6 +138,20 @@ def test_wrapper_say(mockbot, triggerfactory): ) +def test_wrapper_say_statusmsg(mockbot, triggerfactory): + mockbot._isupport = mockbot.isupport.apply( + STATUSMSG=tuple('+'), + ) + + wrapper: bot.SopelWrapper = triggerfactory.wrapper( + mockbot, ':Test!test@example.com PRIVMSG +#channel :test message') + wrapper.say('Hi!') + + assert mockbot.backend.message_sent == rawlist( + 'PRIVMSG +#channel :Hi!' + ) + + def test_wrapper_say_override_destination(mockbot, triggerfactory): wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') @@ -132,6 +172,20 @@ def test_wrapper_notice(mockbot, triggerfactory): ) +def test_wrapper_notice_statusmsg(mockbot, triggerfactory): + mockbot._isupport = mockbot.isupport.apply( + STATUSMSG=tuple('+'), + ) + + wrapper: bot.SopelWrapper = triggerfactory.wrapper( + mockbot, ':Test!test@example.com PRIVMSG +#channel :test message') + wrapper.notice('Hi!') + + assert mockbot.backend.message_sent == rawlist( + 'NOTICE +#channel :Hi!' + ) + + def test_wrapper_notice_override_destination(mockbot, triggerfactory): wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') @@ -152,6 +206,20 @@ def test_wrapper_action(mockbot, triggerfactory): ) +def test_wrapper_action_statusmsg(mockbot, triggerfactory): + mockbot._isupport = mockbot.isupport.apply( + STATUSMSG=tuple('+'), + ) + + wrapper: bot.SopelWrapper = triggerfactory.wrapper( + mockbot, ':Test!test@example.com PRIVMSG +#channel :test message') + wrapper.action('Hi!') + + assert mockbot.backend.message_sent == rawlist( + 'PRIVMSG +#channel :\x01ACTION Hi!\x01' + ) + + def test_wrapper_action_override_destination(mockbot, triggerfactory): wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message') @@ -172,6 +240,20 @@ def test_wrapper_reply(mockbot, triggerfactory): ) +def test_wrapper_reply_statusmsg(mockbot, triggerfactory): + mockbot._isupport = mockbot.isupport.apply( + STATUSMSG=tuple('+'), + ) + + wrapper: bot.SopelWrapper = triggerfactory.wrapper( + mockbot, ':Test!test@example.com PRIVMSG +#channel :test message') + wrapper.reply('Hi!') + + assert mockbot.backend.message_sent == rawlist( + 'PRIVMSG +#channel :Test: Hi!' + ) + + def test_wrapper_reply_override_destination(mockbot, triggerfactory): wrapper = triggerfactory.wrapper( mockbot, ':Test!test@example.com PRIVMSG #channel :test message')