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 e719932c..9b9c82ca 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?") @@ -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.") diff --git a/mmpy_bot/function.py b/mmpy_bot/function.py index c738937e..b9ccdcbd 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 628157d8..f25bc965 100644 --- a/mmpy_bot/settings.py +++ b/mmpy_bot/settings.py @@ -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