add more reactions and add bot replies when user seems angry

This commit is contained in:
HgO 2024-04-17 19:22:42 +02:00
parent 60572d03fa
commit 928d587a45
7 changed files with 175 additions and 6 deletions

View file

@ -23,7 +23,12 @@ from nio.rooms import MatrixRoom
import matrix_alertbot.matrix import matrix_alertbot.matrix
from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.chat_functions import strip_fallback 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 from matrix_alertbot.config import Config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -271,6 +276,27 @@ class Callbacks:
exc_info=e, 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: async def redaction(self, room: MatrixRoom, event: RedactionEvent) -> None:
# Ignore message when we aren't the leader in the client pool # Ignore message when we aren't the leader in the client pool
if self.matrix_client is not self.matrix_client_pool.matrix_client: if self.matrix_client is not self.matrix_client_pool.matrix_client:

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import random
from typing import Optional, Tuple, cast from typing import Optional, Tuple, cast
import pytimeparse2 import pytimeparse2
@ -255,6 +256,29 @@ class HelpCommand(BaseCommand):
await send_text_to_room(self.matrix_client, self.room.room_id, text) 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): class UnknownCommand(BaseCommand):
async def process(self) -> None: async def process(self) -> None:
logger.debug( logger.debug(

View file

@ -21,7 +21,37 @@ logging.getLogger("peewee").setLevel(
) # Prevent debug messages from peewee lib ) # Prevent debug messages from peewee lib
DEFAULT_REACTIONS = {"🤫", "😶", "🤐", "🙊", "🔇", "🔕"} DEFAULT_REACTIONS = {
"🤫",
"😶",
"🤐",
"🙊",
"🔇",
"🔕",
"🚮",
"",
"🚫",
"🤬",
"🫥",
"😶‍🌫️",
"🫣",
"🫢",
"😪",
"😴",
"💤",
"🥱",
"🤌",
"🤏",
"🤚",
"👎",
"🖕",
}
INSULT_REACTIONS = {
"🤬",
"🤌",
"🖕",
}
class AccountConfig: class AccountConfig:
@ -147,6 +177,9 @@ class Config:
self.allowed_reactions = set( self.allowed_reactions = set(
self._get_cfg(["matrix", "allowed_reactions"], default=DEFAULT_REACTIONS) 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.address: str = self._get_cfg(["webhook", "address"], required=False)
self.port: int = self._get_cfg(["webhook", "port"], required=False) self.port: int = self._get_cfg(["webhook", "port"], required=False)

View file

@ -62,7 +62,8 @@ matrix:
- "!abcdefgh:matrix.example.com" - "!abcdefgh:matrix.example.com"
# List of allowed reactions to create silences. # List of allowed reactions to create silences.
allowed_reactions: [🤫, 😶, 🤐] allowed_reactions: [🤫, 😶, 🤐, 🤗]
insult_reactions: [🤗]
webhook: webhook:
# Path to the socket for which the bot should listen to. # Path to the socket for which the bot should listen to.

View file

@ -34,7 +34,8 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# We don't spec config, as it doesn't currently have well defined attributes # We don't spec config, as it doesn't currently have well defined attributes
self.fake_config = Mock() self.fake_config = Mock()
self.fake_config.allowed_rooms = [self.fake_room.room_id] 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.command_prefix = "!alert "
self.fake_config.user_ids = [self.fake_matrix_client.user_id] 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 # Check that we attempted to execute the command
fake_command.assert_not_called() fake_command.assert_not_called()
@patch.object(matrix_alertbot.callback, "AngryUserCommand", autospec=True)
@patch.object(matrix_alertbot.callback, "AckAlertCommand", 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 the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command # Tests that the bot process messages in the room that contain a command
fake_alert_event = Mock(spec=nio.RoomMessageText) 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 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) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_to_inexistent_event(self, fake_command: Mock) -> None: 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 the command prefix"""

View file

@ -1,3 +1,4 @@
import random
import unittest import unittest
from typing import Dict, Optional from typing import Dict, Optional
from unittest.mock import MagicMock, Mock, call, patch 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.alertmanager import AlertmanagerClient
from matrix_alertbot.command import ( from matrix_alertbot.command import (
AckAlertCommand, AckAlertCommand,
AngryUserCommand,
CommandFactory, CommandFactory,
HelpCommand, HelpCommand,
UnackAlertCommand, UnackAlertCommand,
@ -79,6 +81,8 @@ async def delete_silence_raise_silence_not_found_error(silence_id: str) -> None:
class CommandTestCase(unittest.IsolatedAsyncioTestCase): class CommandTestCase(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None: def setUp(self) -> None:
random.seed(42)
# Create a Command object and give it some Mock'd objects to use # 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 = Mock(spec=nio.AsyncClient)
self.fake_matrix_client.user = "@fake_user:example.com" 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 _, _, text = fake_send_text_to_room.call_args.args
self.assertIn("Available commands", text) 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") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_help_with_unknown_topic(self, fake_send_text_to_room: Mock) -> None: async def test_help_with_unknown_topic(self, fake_send_text_to_room: Mock) -> None:
"""Tests the callback for InviteMemberEvents""" """Tests the callback for InviteMemberEvents"""

View file

@ -140,7 +140,8 @@ class ConfigTestCase(unittest.TestCase):
self.assertEqual("https://matrix.domain.tld", config.accounts[1].homeserver_url) self.assertEqual("https://matrix.domain.tld", config.accounts[1].homeserver_url)
self.assertEqual("fake_device_name", config.device_name) self.assertEqual("fake_device_name", config.device_name)
self.assertEqual(["!abcdefgh:matrix.example.com"], config.allowed_rooms) 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.address)
self.assertIsNone(config.port) self.assertIsNone(config.port)