Skip to content

Commit

Permalink
Permit per-plugin/per-listener reply-to-self
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joshuaboniface committed Aug 13, 2024
1 parent d3e0d5e commit cb934b5
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 3 deletions.
49 changes: 49 additions & 0 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<your_bot_token>",
BOT_TEAM = "<team_name>",
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.
4 changes: 1 addition & 3 deletions mmpy_bot/event_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?")
Expand All @@ -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.")
Expand Down
7 changes: 7 additions & 0 deletions mmpy_bot/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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
):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions mmpy_bot/plugins/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
1 change: 1 addition & 0 deletions mmpy_bot/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit cb934b5

Please sign in to comment.