diff --git a/SETUP.md b/SETUP.md index d06bf4e..6312cb0 100644 --- a/SETUP.md +++ b/SETUP.md @@ -112,12 +112,12 @@ matrix-alertbot other-config.yaml Invite the bot to a room and it should accept the invite and join. -Matrix AlertBot will process any message starting with the prefix defined in the config. By default, this prefix is `!alert` . Let's test this now. +Matrix AlertBot will process any message where its name is mentionned. Let's test this now. After the bot has successfully joined the room, try sending the following in a message: ``` -!alert help +@bot_name help ``` The bot should reply with an help message, explaining how to handle alerts. @@ -129,7 +129,7 @@ or by reacting with certain emojis. For instance, if you reply to the alert with: ``` -!alert ack +@bot_name ack ``` This will create a silence for this alert until it is resolved. @@ -138,7 +138,7 @@ You can at any moment reply to the alert with the following to remove the silence: ``` -!alert unack +@bot_name unack ``` Removing a reaction to an alert will also remove the silence. diff --git a/config.sample.yaml b/config.sample.yaml index cd01557..c03c1d6 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -2,9 +2,6 @@ # Below you will find various config sections and options # Default values are shown -# The string to prefix messages with to talk to the bot in group chats -command_prefix: "!alert" - # Options for connecting to the bot's Matrix account matrix: accounts: diff --git a/matrix_alertbot/callback.py b/matrix_alertbot/callback.py index bf5a70d..8db1d3c 100644 --- a/matrix_alertbot/callback.py +++ b/matrix_alertbot/callback.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import re from diskcache import Cache from nio.client import AsyncClient @@ -23,7 +24,12 @@ from nio.rooms import MatrixRoom import matrix_alertbot.matrix from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.chat_functions import strip_fallback -from matrix_alertbot.command import AckAlertCommand, CommandFactory, UnackAlertCommand +from matrix_alertbot.command import ( + AckAlertCommand, + AngryUserCommand, + CommandFactory, + UnackAlertCommand, +) from matrix_alertbot.config import Config logger = logging.getLogger(__name__) @@ -53,7 +59,6 @@ class Callbacks: self.cache = cache self.alertmanager_client = alertmanager_client self.config = config - self.command_prefix = config.command_prefix async def message(self, room: MatrixRoom, event: RoomMessageText) -> None: """Callback for when a message event is received @@ -83,13 +88,22 @@ class Callbacks: f"Event ID {event.event_id} | Sender {event.sender} | " f"Message received: {msg}" ) - # Process as message if in a public room without command prefix - has_command_prefix = msg.startswith(self.command_prefix) - if not has_command_prefix: + + user_id_patterns = [] + for user_id in self.config.user_ids: + user, homeserver = user_id.split(":") + username = user[1:] + user_id_patterns.append(rf"@?{username}(:{homeserver})?") + + pattern = re.compile( + rf"(^|\s+)({'|'.join(user_id_patterns)})(\s+|$)", + re.IGNORECASE | re.MULTILINE, + ) + if pattern.search(msg) is None: logger.debug( f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | " f"Event ID {event.event_id} | Sender {event.sender} | " - f"Cannot process message: Command prefix {self.command_prefix} not provided." + f"Cannot process message: Bot was not mentionned." ) return @@ -107,8 +121,8 @@ class Callbacks: f"Command received is in reply to event ID {reacted_to_event_id}" ) - # Remove the command prefix - cmd = msg[len(self.command_prefix) :] + # Remove the mention of the bot + cmd = pattern.sub(" ", msg).strip() try: command = CommandFactory.create( cmd, @@ -271,6 +285,27 @@ class Callbacks: exc_info=e, ) + if event.key in self.config.insult_reactions: + command = AngryUserCommand( + self.matrix_client, + self.cache, + self.alertmanager_client, + self.config, + room, + event.sender, + event.event_id, + ) + + try: + await command.process() + except (SendRetryError, LocalProtocolError) as e: + logger.exception( + f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | " + f"Event ID {event.event_id} | Sender {event.sender} | " + f"Cannot send message to room.", + exc_info=e, + ) + async def redaction(self, room: MatrixRoom, event: RedactionEvent) -> None: # Ignore message when we aren't the leader in the client pool if self.matrix_client is not self.matrix_client_pool.matrix_client: diff --git a/matrix_alertbot/command.py b/matrix_alertbot/command.py index d8d29f0..7998bcd 100644 --- a/matrix_alertbot/command.py +++ b/matrix_alertbot/command.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import random from typing import Optional, Tuple, cast import pytimeparse2 @@ -237,22 +238,58 @@ class HelpCommand(BaseCommand): async def process(self) -> None: """Show the help text""" logger.debug(f"Displaying help to room {self.room.display_name}") - if not self.args: + if len(self.args) == 0: text = ( - "Hello, I am a bot made with matrix-nio! Use `help commands` to view " + "Hello, I am a bot made with matrix-nio! Use 'help commands' to view " "available commands." ) - await send_text_to_room(self.matrix_client, self.room.room_id, text) - return - - topic = self.args[0] - if topic == "rules": - text = "These are the rules!" - elif topic == "commands": - text = "Available commands: ..." else: - text = "Unknown help topic!" - await send_text_to_room(self.matrix_client, self.room.room_id, text) + topic = self.args[0] + if topic == "commands": + reactions = " ".join( + sorted(self.config.allowed_reactions - self.config.insult_reactions) + ) + text = ( + "Here is the list of available commands:\n" + "- help: Display this help message.\n" + "- ack: Create a silence for the alert that is replied to.\n" + "- unack: Remove a silence for the alert that is replied to.\n\n" + "You can also react with an emoji to an alert to create a silence. " + "Removing a reaction will remove the silence.\n" + f"Here is the list of allowed emoji to trigger a silence: {reactions}\n" + ) + else: + text = ( + "I'm sorry, I don't know much about this topic. " + "You can type 'help commands' to view a list of available commands." + ) + await send_text_to_room( + self.matrix_client, self.room.room_id, text, notice=False + ) + + +class AngryUserCommand(BaseCommand): + async def process(self) -> None: + """React to an insult from the user""" + sender_user_name = self.room.user_name(self.sender) + if sender_user_name is None: + sender_user_name = self.sender + + replies = [ + "You seem upset 😕 Take a deep breath 😌 and a cup of coffee ☕", + "Don't shoot the messenger! 😰", + "You're doing just fine, you're trying your best. If no one ever told you, it's all gonna be okay! 🎶", + ] + random.shuffle(replies) + reply = replies.pop() + + await send_text_to_room( + self.matrix_client, + self.room.room_id, + plaintext=f"{sender_user_name} {reply}", + html=f'{sender_user_name} {reply}', + notice=False, + ) class UnknownCommand(BaseCommand): diff --git a/matrix_alertbot/config.py b/matrix_alertbot/config.py index 450304c..820fc15 100644 --- a/matrix_alertbot/config.py +++ b/matrix_alertbot/config.py @@ -21,7 +21,37 @@ logging.getLogger("peewee").setLevel( ) # Prevent debug messages from peewee lib -DEFAULT_REACTIONS = {"🤫", "😶", "🤐", "🙊", "🔇", "🔕"} +DEFAULT_REACTIONS = { + "🤫", + "😶", + "🤐", + "🙊", + "🔇", + "🔕", + "🚮", + "⛔", + "🚫", + "🤬", + "🫥", + "😶‍🌫️", + "🫣", + "🫢", + "😪", + "😴", + "💤", + "🥱", + "🤌", + "🤏", + "🤚", + "👎", + "🖕", +} + +INSULT_REACTIONS = { + "🤬", + "🤌", + "🖕", +} class AccountConfig: @@ -147,6 +177,9 @@ class Config: self.allowed_reactions = set( self._get_cfg(["matrix", "allowed_reactions"], default=DEFAULT_REACTIONS) ) + self.insult_reactions = set( + self._get_cfg(["matrix", "insult_reactions"], default=INSULT_REACTIONS) + ) self.address: str = self._get_cfg(["webhook", "address"], required=False) self.port: int = self._get_cfg(["webhook", "port"], required=False) @@ -164,10 +197,6 @@ class Config: "Supplied both webhook.socket and both webhook.address" ) - self.command_prefix: str = ( - self._get_cfg(["command_prefix"], default="!alert") + " " - ) - def _get_cfg( self, path: List[str], diff --git a/tests/resources/config/config.full.yml b/tests/resources/config/config.full.yml index bb163d0..7119d7e 100644 --- a/tests/resources/config/config.full.yml +++ b/tests/resources/config/config.full.yml @@ -2,9 +2,6 @@ # Below you will find various config sections and options # Default values are shown -# The string to prefix messages with to talk to the bot in group chats -command_prefix: "!alert" - # Options for connecting to the bot's Matrix account matrix: accounts: @@ -62,7 +59,8 @@ matrix: - "!abcdefgh:matrix.example.com" # List of allowed reactions to create silences. - allowed_reactions: [🤫, 😶, 🤐] + allowed_reactions: [🤫, 😶, 🤐, 🤗] + insult_reactions: [🤗] webhook: # Path to the socket for which the bot should listen to. diff --git a/tests/test_callback.py b/tests/test_callback.py index bd0b771..4fdbab1 100644 --- a/tests/test_callback.py +++ b/tests/test_callback.py @@ -34,8 +34,8 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # We don't spec config, as it doesn't currently have well defined attributes self.fake_config = Mock() self.fake_config.allowed_rooms = [self.fake_room.room_id] - self.fake_config.allowed_reactions = ["🤫"] - self.fake_config.command_prefix = "!alert " + self.fake_config.allowed_reactions = ["🤫", "🤗"] + self.fake_config.insult_reactions = ["🤗"] self.fake_config.user_ids = [self.fake_matrix_client.user_id] self.fake_matrix_client_pool = Mock( @@ -100,8 +100,8 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): ) @patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True) - async def test_message_without_prefix(self, fake_command_create: Mock) -> None: - """Tests the callback for RoomMessageText without any command prefix""" + async def test_message_without_mention(self, fake_command_create: Mock) -> None: + """Tests the callback for RoomMessageText without any mention of the bot""" # Tests that the bot process messages in the room fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.sender = "@some_other_fake_user:example.com" @@ -116,12 +116,12 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.command, "HelpCommand", autospec=True) async def test_message_help_client_not_in_pool(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText without any command prefix""" + """Tests the callback for RoomMessageText without any mention of the bot""" # Tests that the bot process messages in the room fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert help" + fake_message_event.body = "@fake_user help" fake_message_event.source = {"content": {}} self.fake_matrix_client_pool.matrix_client = None @@ -133,15 +133,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_command.assert_not_called() @patch.object(matrix_alertbot.command, "HelpCommand", autospec=True) - async def test_message_help_not_in_reply_with_prefix( + async def test_message_help_not_in_reply_with_mention( self, fake_command: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert help" + fake_message_event.body = "@fake_user help" fake_message_event.source = {"content": {}} # Pretend that we received a text message event @@ -161,14 +161,14 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_command.return_value.process.assert_called_once() @patch.object(matrix_alertbot.command, "HelpCommand", autospec=True) - async def test_message_help_in_reply_with_prefix(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + async def test_message_help_in_reply_with_mention(self, fake_command: Mock) -> None: + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert help" + fake_message_event.body = "@fake_user help" fake_message_event.source = { "content": { "m.relates_to": {"m.in_reply_to": {"event_id": "some alert event id"}} @@ -193,7 +193,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.command.CommandFactory, "create", autospec=True) async def test_ignore_message_sent_by_bot(self, fake_create_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) @@ -209,7 +209,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_ignore_message_sent_on_unauthorized_room( self, fake_create_command: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command self.fake_room.room_id = "!unauthorizedroom@example.com" @@ -224,15 +224,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_create_command.assert_not_called() @patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) - async def test_message_ack_not_in_reply_with_prefix( + async def test_message_ack_not_in_reply_with_mention( self, fake_command: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert ack" + fake_message_event.body = "@fake_user ack" fake_message_event.source = {"content": {}} # Pretend that we received a text message event @@ -242,13 +242,48 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_command.assert_not_called() @patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) - async def test_message_ack_in_reply_with_prefix(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + async def test_message_ack_in_reply_with_full_mention( + self, fake_command: Mock + ) -> None: + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert ack" + fake_message_event.body = "@fake_user:example.com ack" + fake_message_event.source = { + "content": { + "m.relates_to": {"m.in_reply_to": {"event_id": "some alert event id"}} + } + } + + # Pretend that we received a text message event + await self.callbacks.message(self.fake_room, fake_message_event) + + # Check that the command was not executed + fake_command.assert_called_once_with( + self.fake_matrix_client, + self.fake_cache, + self.fake_alertmanager_client, + self.fake_config, + self.fake_room, + fake_message_event.sender, + fake_message_event.event_id, + "some alert event id", + (), + ) + fake_command.return_value.process.assert_called_once() + + @patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) + async def test_message_ack_in_reply_with_short_mention( + self, fake_command: Mock + ) -> None: + """Tests the callback for RoomMessageText with a mention of the bot""" + # Tests that the bot process messages in the room that contain a command + fake_message_event = Mock(spec=nio.RoomMessageText) + fake_message_event.event_id = "some event id" + fake_message_event.sender = "@some_other_fake_user:example.com" + fake_message_event.body = "fake_user ack" fake_message_event.source = { "content": { "m.relates_to": {"m.in_reply_to": {"event_id": "some alert event id"}} @@ -273,15 +308,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_command.return_value.process.assert_called_once() @patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True) - async def test_message_unack_not_in_reply_with_prefix( + async def test_message_unack_not_in_reply_with_mention( self, fake_command: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert unack" + fake_message_event.body = "@fake_user unack" fake_message_event.source = {"content": {}} # Pretend that we received a text message event @@ -291,13 +326,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_command.assert_not_called() @patch.object(matrix_alertbot.command, "UnackAlertCommand", autospec=True) - async def test_message_unack_in_reply_with_prefix(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + async def test_message_unack_in_reply_with_mention( + self, fake_command: Mock + ) -> None: + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert unack" + fake_message_event.body = "@fake_user unack" fake_message_event.source = { "content": { "m.relates_to": {"m.in_reply_to": {"event_id": "some alert event id"}} @@ -326,12 +363,12 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_message_raise_exception( self, fake_command: Mock, fake_logger ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) fake_message_event.event_id = "some event id" fake_message_event.sender = "@some_other_fake_user:example.com" - fake_message_event.body = "!alert ack" + fake_message_event.body = "@fake_user ack" fake_message_event.source = { "content": { "m.relates_to": {"m.in_reply_to": {"event_id": "some alert event id"}} @@ -363,7 +400,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) async def test_reaction_client_not_in_pool(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event = Mock(spec=nio.RoomMessageText) fake_alert_event.event_id = "some alert event id" @@ -387,9 +424,12 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_not_called() + @patch.object(matrix_alertbot.callback, "AngryUserCommand", autospec=True) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) - async def test_reaction_to_existing_alert(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + async def test_reaction_to_existing_alert( + self, fake_command: Mock, fake_angry_user_command + ) -> None: + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event = Mock(spec=nio.RoomMessageText) fake_alert_event.event_id = "some alert event id" @@ -424,9 +464,62 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_room.room_id, fake_alert_event.event_id ) + fake_angry_user_command.assert_not_called() + + @patch.object(matrix_alertbot.callback, "AngryUserCommand", autospec=True) + @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) + async def test_insult_reaction( + self, fake_command: Mock, fake_angry_user_command: Mock + ) -> None: + """Tests the callback for RoomMessageText with a mention of the bot""" + # Tests that the bot process messages in the room that contain a command + fake_alert_event = Mock(spec=nio.RoomMessageText) + fake_alert_event.event_id = "some alert event id" + fake_alert_event.sender = self.fake_matrix_client.user_id + + fake_reaction_event = Mock(spec=nio.ReactionEvent) + fake_reaction_event.event_id = "some event id" + fake_reaction_event.sender = "@some_other_fake_user:example.com" + fake_reaction_event.reacts_to = fake_alert_event.event_id + fake_reaction_event.key = "🤗" + + fake_event_response = Mock(spec=nio.RoomGetEventResponse) + fake_event_response.event = fake_alert_event + self.fake_matrix_client.room_get_event.return_value = fake_event_response + + # Pretend that we received a text message event + await self.callbacks.reaction(self.fake_room, fake_reaction_event) + + # Check that we attempted to execute the command + fake_command.assert_called_once_with( + self.fake_matrix_client, + self.fake_cache, + self.fake_alertmanager_client, + self.fake_config, + self.fake_room, + fake_reaction_event.sender, + fake_reaction_event.event_id, + "some alert event id", + ) + fake_command.return_value.process.assert_called_once() + self.fake_matrix_client.room_get_event.assert_called_once_with( + self.fake_room.room_id, fake_alert_event.event_id + ) + + fake_angry_user_command.assert_called_once_with( + self.fake_matrix_client, + self.fake_cache, + self.fake_alertmanager_client, + self.fake_config, + self.fake_room, + fake_reaction_event.sender, + fake_reaction_event.event_id, + ) + fake_command.return_value.process.assert_called_once() + @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) async def test_reaction_to_inexistent_event(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event_id = "some alert event id" @@ -454,7 +547,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_reaction_to_event_not_from_bot_user( self, fake_command: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event = Mock(spec=nio.RoomMessageText) fake_alert_event.event_id = "some alert event id" @@ -486,7 +579,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_reaction_raise_exception( self, fake_command: Mock, fake_logger: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event = Mock(spec=nio.RoomMessageText) fake_alert_event.event_id = "some alert event id" @@ -529,7 +622,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) async def test_reaction_unknown(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event_id = "some alert event id" @@ -549,7 +642,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) async def test_ignore_reaction_sent_by_bot_user(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event_id = "some alert event id" @@ -571,7 +664,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_ignore_reaction_in_unauthorized_room( self, fake_command: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command self.fake_room.room_id = "!unauthorizedroom@example.com" @@ -593,7 +686,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True) async def test_redaction_client_not_in_pool(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event_id = "some alert event id" @@ -615,7 +708,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True) async def test_redaction(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event_id = "some alert event id" @@ -648,7 +741,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_redaction_raise_exception( self, fake_command: Mock, fake_logger ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_alert_event_id = "some alert event id" @@ -684,7 +777,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True) async def test_ignore_redaction_sent_by_bot_user(self, fake_command: Mock) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_redaction_event = Mock(spec=nio.RedactionEvent) fake_redaction_event.sender = self.fake_matrix_client.user_id @@ -703,7 +796,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_ignore_redaction_in_unauthorized_room( self, fake_command: Mock ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command self.fake_room.room_id = "!unauthorizedroom@example.com" @@ -721,7 +814,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_cache.__getitem__.assert_not_called() async def test_key_verification_start(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -745,7 +838,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_matrix_client.to_device.assert_called_once_with(fake_sas.share_key()) async def test_key_verification_start_with_emoji_not_supported(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -769,7 +862,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_key_verification_start_with_accept_key_verification_error( self, ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -799,7 +892,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): async def test_key_verification_start_with_to_device_error( self, ) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -825,7 +918,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_matrix_client.to_device.assert_called_once_with(fake_sas.share_key()) async def test_key_verification_cancel(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_key_verification_event = Mock(spec=nio.KeyVerificationCancel) fake_key_verification_event.sender = "@some_other_fake_user:example.com" @@ -837,7 +930,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command async def test_key_verification_confirm(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -862,7 +955,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): ) async def test_key_verification_confirm_with_error(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -891,7 +984,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): ) async def test_key_verification_end(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -912,7 +1005,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_matrix_client.to_device.assert_called_once_with(fake_sas.get_mac()) async def test_key_verification_end_with_missing_transaction_id(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -932,7 +1025,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_matrix_client.to_device.assert_not_called() async def test_key_verification_end_with_mac_error(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" @@ -953,7 +1046,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_matrix_client.to_device.assert_not_called() async def test_key_verification_end_with_to_device_error(self) -> None: - """Tests the callback for RoomMessageText with the command prefix""" + """Tests the callback for RoomMessageText with a mention of the bot""" # Tests that the bot process messages in the room that contain a command fake_transaction_id = "fake transaction id" diff --git a/tests/test_command.py b/tests/test_command.py index b7db170..4d43c1b 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,3 +1,4 @@ +import random import unittest from typing import Dict, Optional from unittest.mock import MagicMock, Mock, call, patch @@ -10,6 +11,7 @@ import matrix_alertbot.command from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.command import ( AckAlertCommand, + AngryUserCommand, CommandFactory, HelpCommand, UnackAlertCommand, @@ -79,6 +81,8 @@ async def delete_silence_raise_silence_not_found_error(silence_id: str) -> None: class CommandTestCase(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: + random.seed(42) + # Create a Command object and give it some Mock'd objects to use self.fake_matrix_client = Mock(spec=nio.AsyncClient) self.fake_matrix_client.user = "@fake_user:example.com" @@ -105,7 +109,8 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # We don't spec config, as it doesn't currently have well defined attributes self.fake_config = Mock() self.fake_config.allowed_rooms = [self.fake_room.room_id] - self.fake_config.command_prefix = "!alert " + self.fake_config.allowed_reactions = {"🤫", "😶", "🤐", "🤗"} + self.fake_config.insult_reactions = {"🤗"} @patch.object(matrix_alertbot.command.AckAlertCommand, "process") async def test_process_ack_command(self, fake_ack: Mock) -> None: @@ -645,29 +650,6 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): _, _, text = fake_send_text_to_room.call_args.args self.assertIn("help commands", text) - @patch.object(matrix_alertbot.command, "send_text_to_room") - async def test_help_with_rules_topic(self, fake_send_text_to_room: Mock) -> None: - """Tests the callback for InviteMemberEvents""" - # Tests that the bot attempts to join a room after being invited to it - - command = HelpCommand( - self.fake_matrix_client, - self.fake_cache, - self.fake_alertmanager_client, - self.fake_config, - self.fake_room, - self.fake_sender, - self.fake_event_id, - ("rules",), - ) - - await command.process() - - # Check that we attempted to create silences - fake_send_text_to_room.assert_called_once() - _, _, text = fake_send_text_to_room.call_args.args - self.assertIn("rules!", text) - @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_help_with_commands_topic(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" @@ -689,7 +671,43 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to create silences fake_send_text_to_room.assert_called_once() _, _, text = fake_send_text_to_room.call_args.args - self.assertIn("Available commands", text) + self.assertIn("Here is the list of available commands", text) + reactions = ( + self.fake_config.allowed_reactions - self.fake_config.insult_reactions + ) + for reaction in reactions: + self.assertIn(reaction, text) + for reaction in self.fake_config.insult_reactions: + self.assertNotIn(reaction, text) + + @patch.object(matrix_alertbot.command, "send_text_to_room") + async def test_angry_user(self, fake_send_text_to_room: Mock) -> None: + """Tests the callback for InviteMemberEvents""" + # Tests that the bot attempts to join a room after being invited to it + + command = AngryUserCommand( + self.fake_matrix_client, + self.fake_cache, + self.fake_alertmanager_client, + self.fake_config, + self.fake_room, + self.fake_sender, + self.fake_event_id, + ) + + await command.process() + + # Check that we attempted to create silences + fake_send_text_to_room.assert_called_once() + text, html, _ = fake_send_text_to_room.call_args.kwargs.values() + self.assertRegex( + text, + "^@some_other_fake_user:example.com ", + ) + self.assertRegex( + html, + '^@some_other_fake_user:example.com ', + ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_help_with_unknown_topic(self, fake_send_text_to_room: Mock) -> None: @@ -712,7 +730,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to create silences fake_send_text_to_room.assert_called_once() _, _, text = fake_send_text_to_room.call_args.args - self.assertEqual("Unknown help topic!", text) + self.assertIn("I'm sorry, I don't know much about this topic.", text) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unknown_command(self, fake_send_text_to_room: Mock) -> None: diff --git a/tests/test_config.py b/tests/test_config.py index 966f93d..685c3f3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -91,8 +91,6 @@ class ConfigTestCase(unittest.TestCase): self.assertIsNone(config.template_dir) - self.assertEqual("!alert ", config.command_prefix) - @patch("os.path.isdir") @patch("os.path.exists") @patch("os.mkdir") @@ -140,7 +138,8 @@ class ConfigTestCase(unittest.TestCase): self.assertEqual("https://matrix.domain.tld", config.accounts[1].homeserver_url) self.assertEqual("fake_device_name", config.device_name) self.assertEqual(["!abcdefgh:matrix.example.com"], config.allowed_rooms) - self.assertEqual({"🤫", "😶", "🤐"}, config.allowed_reactions) + self.assertEqual({"🤫", "😶", "🤐", "🤗"}, config.allowed_reactions) + self.assertEqual({"🤗"}, config.insult_reactions) self.assertIsNone(config.address) self.assertIsNone(config.port) @@ -156,8 +155,6 @@ class ConfigTestCase(unittest.TestCase): self.assertEqual("data/templates", config.template_dir) - self.assertEqual("!alert ", config.command_prefix) - def test_read_config_raise_config_error(self) -> None: with self.assertRaises(ParseConfigError): Config("") diff --git a/tests/test_matrix.py b/tests/test_matrix.py index 4555bde..dd09981 100644 --- a/tests/test_matrix.py +++ b/tests/test_matrix.py @@ -62,7 +62,6 @@ class MatrixClientPoolTestCase(unittest.IsolatedAsyncioTestCase): self.fake_account_config_2.token_file = "account2.token.secret" self.fake_config = Mock(spec=Config) self.fake_config.store_dir = "/dev/null" - self.fake_config.command_prefix = "!alert" self.fake_config.accounts = [ self.fake_account_config_1, self.fake_account_config_2,