Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permit per-plugin/per-listener reply-to-self #544

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -39,7 +37,7 @@ def _should_ignore(self, message: Message):
return (
message.sender_name.lower()
in (name.lower() for name in self.settings.IGNORE_USERS)
) 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 @@ -63,6 +63,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