From cb934b53981d225cb6dd8156df135183be13798d Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Tue, 13 Aug 2024 10:20:34 -0400 Subject: [PATCH] Permit per-plugin/per-listener reply-to-self This commit implements a per-plugin reply-to-self functionality, allowing the bot to, for selected `listen_to` statements, reply to its own previous messages. For example, this would allow the bot to send a reply like "Hey @sender do the thing" to a trigger, and then handle its own "@sender" listener for that message. First, we turn the `EventHandler` `ignore_own_messages` into a global `mmpy_bot` setting, which defaults to `True` to preserve the existing behaviour; the bot user must turn this off to use this new feature. Second, we add a handler within the `MessageFunction` that duplicates the functionality in `EventHandler`; this ensures that the existing behaviour continues to be preserved, even if the global setting is `False`. Finally, we add another kwarg to the `listen_to` decorator to permit passing an `ignore_own_message=False` option, which would then allow that particular listener to ignore the previous conditions and reply to itself. Information about this functionality, including a warning about loops, has been added to the documentation on Plugins at the bottom of the page, to ensure users become aware of this new feature and what needs to be done to activate it. --- docs/plugins.rst | 49 +++++++++++++++++++++++++++++++++++++ mmpy_bot/event_handler.py | 4 +-- mmpy_bot/function.py | 7 ++++++ mmpy_bot/plugins/example.py | 5 ++++ mmpy_bot/settings.py | 1 + 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index e17247a1..101c1933 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -385,3 +385,52 @@ You should notice that this method takes a datetime object, which is different f The following code example uses `schedule.once` to schedule a job. This job will be trigger at `t_time`. + +Bot replies to its own messages +------------------------------- + +By default, the bot will never reply to its own messages, to avoid loops and other potentially undefined behaviour. + +However, it might be useful to occasionally create listeners that can be triggered by the bot's own replies, for +example a custom @-ping that the bot itself might make. + +Achieving this requires 2 setup steps: + +1. In the global ``mmpy_bot`` ``Settings``, set the ``IGNORE_OWN_MESSAGES`` argument to ``False``: + + .. code-block:: python + + #!/usr/bin/env python + + from mmpy_bot import Bot, Settings + from my_plugin import MyPlugin + + bot = Bot( + settings=Settings( + MATTERMOST_URL = "http://127.0.0.1", + MATTERMOST_PORT = 8065, + BOT_TOKEN = "", + BOT_TEAM = "", + SSL_VERIFY = False, + IGNORE_OWN_MESSAGES = False, + ), # Either specify your settings here or as environment variables. + plugins=[MyPlugin()], # Add your own plugins here. + ) + bot.run() + + **NOTE:** This is safe, and will not trigger any loops by itself; the default bot behaviour is still to ignore + its own messages within each listener. + +2. For the listeners that you want to be able to reply to the bot's own messages, add an ``ignore_own_messages=False`` +keyword argument to the ``listen_to`` decorator: + + .. code-block:: python + + @listen_to("^poke$", ignore_own_message=False) + async def poke(self, message: Message): + """Will reply to any instance of "poke" even if sent by the bot itself.""" + self.driver.reply_to(message, f"Hello, @{message.sender_name}!") + +**WARNING:** When using this functionality, be careful of the potential to cause infinite message loops! You **must** +ensure that any listener using ``ignore_own_messages=False`` cannot itself trigger another listener, especially +itself. diff --git a/mmpy_bot/event_handler.py b/mmpy_bot/event_handler.py index 7d70466a..970a8c2e 100644 --- a/mmpy_bot/event_handler.py +++ b/mmpy_bot/event_handler.py @@ -19,13 +19,11 @@ def __init__( driver: Driver, settings: Settings, plugin_manager: PluginManager, - ignore_own_messages=True, ): """The EventHandler class takes care of the connection to mattermost and calling the appropriate response function to each event.""" self.driver = driver self.settings = settings - self.ignore_own_messages = ignore_own_messages self.plugin_manager = plugin_manager self._name_matcher = re.compile(rf"^@?{self.driver.username}[:,]?\s?") @@ -41,7 +39,7 @@ def _should_ignore(self, message: Message): if message.sender_name.lower() in (name.lower() for name in self.settings.IGNORE_USERS) else False - ) or (self.ignore_own_messages and message.sender_name == self.driver.username) + ) or (self.settings.IGNORE_OWN_MESSAGES and message.sender_name == self.driver.username) async def _check_queue_loop(self, webhook_queue: queue.Queue): log.info("EventHandlerWebHook queue listener started.") diff --git a/mmpy_bot/function.py b/mmpy_bot/function.py index 7b6b0ded..6d0acfee 100644 --- a/mmpy_bot/function.py +++ b/mmpy_bot/function.py @@ -73,6 +73,7 @@ def __init__( *args, direct_only: bool = False, needs_mention: bool = False, + ignore_own_messages: bool = True, silence_fail_msg: bool = False, allowed_users: Optional[Sequence[str]] = None, allowed_channels: Optional[Sequence[str]] = None, @@ -83,6 +84,7 @@ def __init__( self.is_click_function = isinstance(self.function, click.Command) self.direct_only = direct_only self.needs_mention = needs_mention + self.ignore_own_messages = ignore_own_messages self.silence_fail_msg = silence_fail_msg if allowed_users is None: @@ -132,6 +134,9 @@ def __call__(self, message: Message, *args): if self.direct_only and not message.is_direct_message: return return_value + if self.ignore_own_messages and (message.sender_name == self.plugin.driver.username): + return return_value + if self.needs_mention and not ( message.is_direct_message or self.plugin.driver.user_id in message.mentions ): @@ -179,6 +184,7 @@ def listen_to( *, direct_only=False, needs_mention=False, + ignore_own_messages=True, allowed_users=None, allowed_channels=None, silence_fail_msg=False, @@ -214,6 +220,7 @@ def wrapped_func(func): matcher=pattern, direct_only=direct_only, needs_mention=needs_mention, + ignore_own_messages=ignore_own_messages, allowed_users=allowed_users, allowed_channels=allowed_channels, silence_fail_msg=silence_fail_msg, diff --git a/mmpy_bot/plugins/example.py b/mmpy_bot/plugins/example.py index 8850a9f1..8acde7e3 100644 --- a/mmpy_bot/plugins/example.py +++ b/mmpy_bot/plugins/example.py @@ -171,3 +171,8 @@ async def sleep_reply(self, message: Message, seconds: str): self.driver.reply_to(message, f"Okay, I will be waiting {seconds} seconds.") await asyncio.sleep(int(seconds)) self.driver.reply_to(message, "Done!") + + @listen_to("^@thisuser$", re.IGNORECASE, ignore_own_messages=False) + def custom_ping_replytoself(self, message: Message): + """Demonstration of ignore_own_messages, requires global settings IGNORE_OWN_MESSAGES = False""" + self.driver.reply_to(message, f"Hello @{message.sender_name}") diff --git a/mmpy_bot/settings.py b/mmpy_bot/settings.py index 4b2753cf..fd7f7823 100644 --- a/mmpy_bot/settings.py +++ b/mmpy_bot/settings.py @@ -65,6 +65,7 @@ class Settings: LOG_FORMAT: str = "[%(asctime)s][%(name)s][%(levelname)s] %(message)s" LOG_DATE_FORMAT: str = "%m/%d/%Y %H:%M:%S" + IGNORE_OWN_MESSAGES: bool = True IGNORE_USERS: Sequence[str] = field(default_factory=list) # How often to check whether any scheduled jobs need to be run, default every second SCHEDULER_PERIOD: float = 1.0