From 928d587a45017fc7d7363cfd1f2dd792b7f786a5 Mon Sep 17 00:00:00 2001 From: HgO Date: Wed, 17 Apr 2024 19:22:42 +0200 Subject: [PATCH] add more reactions and add bot replies when user seems angry --- matrix_alertbot/callback.py | 28 +++++++++++- matrix_alertbot/command.py | 24 +++++++++++ matrix_alertbot/config.py | 35 ++++++++++++++- tests/resources/config/config.full.yml | 3 +- tests/test_callback.py | 59 +++++++++++++++++++++++++- tests/test_command.py | 29 +++++++++++++ tests/test_config.py | 3 +- 7 files changed, 175 insertions(+), 6 deletions(-) diff --git a/matrix_alertbot/callback.py b/matrix_alertbot/callback.py index bf5a70d..378d68f 100644 --- a/matrix_alertbot/callback.py +++ b/matrix_alertbot/callback.py @@ -23,7 +23,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__) @@ -271,6 +276,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..a714d74 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 @@ -255,6 +256,29 @@ class HelpCommand(BaseCommand): await send_text_to_room(self.matrix_client, self.room.room_id, text) +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, + f"{sender_user_name} {reply}", + notice=False, + ) + + class UnknownCommand(BaseCommand): async def process(self) -> None: logger.debug( diff --git a/matrix_alertbot/config.py b/matrix_alertbot/config.py index 450304c..aabf2ea 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) diff --git a/tests/resources/config/config.full.yml b/tests/resources/config/config.full.yml index bb163d0..ce3e341 100644 --- a/tests/resources/config/config.full.yml +++ b/tests/resources/config/config.full.yml @@ -62,7 +62,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..2dffd0e 100644 --- a/tests/test_callback.py +++ b/tests/test_callback.py @@ -34,7 +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.allowed_reactions = ["🤫", "🤗"] + self.fake_config.insult_reactions = ["🤗"] self.fake_config.command_prefix = "!alert " self.fake_config.user_ids = [self.fake_matrix_client.user_id] @@ -387,8 +388,9 @@ 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: + async def test_reaction_to_existing_alert(self, fake_command: Mock, fake_angry_user_command) -> None: """Tests the callback for RoomMessageText with the command prefix""" # Tests that the bot process messages in the room that contain a command fake_alert_event = Mock(spec=nio.RoomMessageText) @@ -424,6 +426,59 @@ 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 the command prefix""" + # 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""" diff --git a/tests/test_command.py b/tests/test_command.py index b7db170..0eabeaf 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" @@ -691,6 +695,31 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): _, _, text = fake_send_text_to_room.call_args.args self.assertIn("Available commands", 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 = fake_send_text_to_room.call_args.args + self.assertRegex( + text, + "^@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: """Tests the callback for InviteMemberEvents""" diff --git a/tests/test_config.py b/tests/test_config.py index 966f93d..eaf9646 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -140,7 +140,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)