matrix-alertbot/matrix_alertbot/callback.py

512 lines
18 KiB
Python
Raw Normal View History

2024-01-22 11:35:13 +01:00
from __future__ import annotations
2020-08-10 00:02:07 +02:00
import logging
import re
2020-08-10 00:02:07 +02:00
2022-07-06 00:54:13 +02:00
from diskcache import Cache
2024-01-22 11:35:13 +01:00
from nio.client import AsyncClient
from nio.events import (
2021-01-10 04:30:07 +01:00
InviteMemberEvent,
2022-10-26 13:25:47 +02:00
KeyVerificationCancel,
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationStart,
2021-01-10 04:30:07 +01:00
MegolmEvent,
2024-01-22 11:35:13 +01:00
ReactionEvent,
2022-07-10 14:42:23 +02:00
RedactionEvent,
2021-01-10 04:30:07 +01:00
RoomMessageText,
2024-01-22 11:35:13 +01:00
RoomMessageUnknown,
2021-01-10 04:30:07 +01:00
)
2024-01-22 11:35:13 +01:00
from nio.exceptions import LocalProtocolError, SendRetryError
from nio.responses import JoinError, RoomGetEventError, RoomSendError, ToDeviceError
from nio.rooms import MatrixRoom
2020-08-10 00:02:07 +02:00
2024-01-22 11:35:13 +01:00
import matrix_alertbot.matrix
from matrix_alertbot.alertmanager import AlertmanagerClient
2022-07-10 13:13:32 +02:00
from matrix_alertbot.chat_functions import strip_fallback
from matrix_alertbot.command import (
AckAlertCommand,
AngryUserCommand,
CommandFactory,
UnackAlertCommand,
)
2022-06-13 20:55:01 +02:00
from matrix_alertbot.config import Config
2019-09-25 14:26:29 +02:00
logger = logging.getLogger(__name__)
2021-01-10 04:30:07 +01:00
class Callbacks:
def __init__(
self,
2022-08-08 00:28:36 +02:00
matrix_client: AsyncClient,
alertmanager_client: AlertmanagerClient,
cache: Cache,
config: Config,
2024-01-22 11:35:13 +01:00
matrix_client_pool: matrix_alertbot.matrix.MatrixClientPool,
):
2019-09-25 14:26:29 +02:00
"""
Args:
client: nio client used to interact with matrix.
2019-09-25 14:26:29 +02:00
cache: Bot cache.
alertmanager: Client used to interact with alertmanager.
2019-09-25 14:26:29 +02:00
config: Bot configuration parameters.
2019-09-25 14:26:29 +02:00
"""
2022-08-08 00:28:36 +02:00
self.matrix_client = matrix_client
2024-01-22 11:35:13 +01:00
self.matrix_client_pool = matrix_client_pool
self.cache = cache
2022-08-08 00:28:36 +02:00
self.alertmanager_client = alertmanager_client
self.config = config
2019-09-25 14:26:29 +02:00
2021-01-10 04:30:07 +01:00
async def message(self, room: MatrixRoom, event: RoomMessageText) -> None:
2019-09-25 14:26:29 +02:00
"""Callback for when a message event is received
Args:
room: The room the event came from.
2019-09-25 14:26:29 +02:00
event: The event defining the message.
2019-09-25 14:26:29 +02:00
"""
2024-01-22 11:35:13 +01:00
# Ignore message when we aren't the leader in the client pool
if self.matrix_client is not self.matrix_client_pool.matrix_client:
return
2019-09-25 14:26:29 +02:00
# Ignore messages from ourselves
2024-01-22 11:35:13 +01:00
if event.sender in self.config.user_ids:
2019-09-25 14:26:29 +02:00
return
# Ignore messages from unauthorized room
if room.room_id not in self.config.allowed_rooms:
return
# Extract the message text
msg = strip_fallback(event.body)
2019-09-25 14:26:29 +02:00
logger.debug(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Message received: {msg}"
2019-09-25 14:26:29 +02:00
)
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(
2024-04-20 17:50:56 +02:00
rf"(^|\s+)({'|'.join(user_id_patterns)}):?(?=\s+|$)",
re.IGNORECASE | re.MULTILINE,
)
if pattern.search(msg) is None:
logger.debug(
2024-01-22 11:35:13 +01:00
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: Bot was not mentionned."
)
2019-09-25 14:26:29 +02:00
return
2022-07-10 14:06:36 +02:00
source_content = event.source["content"]
reacted_to_event_id = (
source_content.get("m.relates_to", {})
.get("m.in_reply_to", {})
.get("event_id")
)
if reacted_to_event_id is not None:
2024-01-22 11:35:13 +01:00
logger.debug(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Command received is in reply to event ID {reacted_to_event_id}"
)
2022-07-10 14:06:36 +02:00
# Remove the mention of the bot
cmd = pattern.sub(" ", msg).strip()
2024-04-20 17:50:56 +02:00
logger.debug(
"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Processing command {cmd}"
)
2022-07-16 23:20:25 +02:00
try:
command = CommandFactory.create(
cmd,
2022-08-08 00:28:36 +02:00
self.matrix_client,
2022-07-16 23:20:25 +02:00
self.cache,
2022-08-08 00:28:36 +02:00
self.alertmanager_client,
2022-07-16 23:20:25 +02:00
self.config,
room,
event.sender,
event.event_id,
reacted_to_event_id,
2022-07-16 23:20:25 +02:00
)
except TypeError as e:
2024-01-22 11:35:13 +01:00
logger.error(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Cannot process command '{cmd}': {e}"
)
2022-07-16 23:20:25 +02:00
return
2022-08-08 12:38:09 +02:00
try:
await command.process()
except (SendRetryError, LocalProtocolError) as e:
2024-01-22 11:35:13 +01:00
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,
)
2019-09-25 14:26:29 +02:00
2021-01-10 04:30:07 +01:00
async def invite(self, room: MatrixRoom, event: InviteMemberEvent) -> None:
"""Callback for when an invite is received. Join the room specified in the invite.
Args:
room: The room that we are invited to.
event: The invite event.
"""
# Ignore invites from unauthorized room
if room.room_id not in self.config.allowed_rooms:
return
2024-01-22 11:35:13 +01:00
logger.debug(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Sender {event.sender} | "
f"Invitation received."
)
2019-09-25 14:26:29 +02:00
# Attempt to join 3 times before giving up
for attempt in range(3):
2022-08-08 00:28:36 +02:00
result = await self.matrix_client.join(room.room_id)
2024-01-22 11:43:11 +01:00
if isinstance(result, JoinError):
2019-09-25 14:26:29 +02:00
logger.error(
2024-01-22 11:43:11 +01:00
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Sender {event.sender} | "
f"Error joining room (attempt {attempt}): {result.message}"
2019-09-25 14:26:29 +02:00
)
else:
break
2020-06-23 03:11:31 +02:00
else:
2024-01-22 11:43:11 +01:00
logger.error(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Sender {event.sender} | "
f"Unable to join room"
)
2019-09-25 14:26:29 +02:00
2020-06-23 03:11:31 +02:00
# Successfully joined room
2024-01-22 11:35:13 +01:00
logger.info(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Sender {event.sender} | "
f"Room joined."
)
async def invite_event_filtered_callback(
self, room: MatrixRoom, event: InviteMemberEvent
) -> None:
"""
Since the InviteMemberEvent is fired for every m.room.member state received
in a sync response's `rooms.invite` section, we will receive some that are
not actually our own invite event (such as the inviter's membership).
This makes sure we only call `callbacks.invite` with our own invite events.
"""
2022-08-08 00:28:36 +02:00
if event.state_key == self.matrix_client.user_id:
# This is our own membership (invite) event
await self.invite(room, event)
2024-01-22 11:35:13 +01:00
async def reaction(self, room: MatrixRoom, event: ReactionEvent) -> None:
"""A reaction was sent to one of our messages. Let's send a reply acknowledging it.
Args:
room: The room the reaction was sent in.
event: The reaction event.
reacted_to_id: The event ID that the reaction points to.
"""
2024-01-22 11:35:13 +01:00
# Ignore message when we aren't the leader in the client pool
if self.matrix_client is not self.matrix_client_pool.matrix_client:
return
# Ignore reactions from unauthorized room
if room.room_id not in self.config.allowed_rooms:
return
# Ignore reactions from ourselves
2024-01-22 11:35:13 +01:00
if event.sender in self.config.user_ids:
return
2024-01-22 11:35:13 +01:00
logger.debug(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Reaction received: {event.key}"
)
2022-07-10 13:13:32 +02:00
2024-01-22 11:35:13 +01:00
if event.key not in self.config.allowed_reactions:
logger.warning(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Reaction not handled: {event.key}"
)
2022-07-10 13:13:32 +02:00
return
2024-01-22 11:35:13 +01:00
alert_event_id = event.reacts_to
# Get the original event that was reacted to
2022-08-08 00:28:36 +02:00
event_response = await self.matrix_client.room_get_event(
room.room_id, alert_event_id
)
if isinstance(event_response, RoomGetEventError):
logger.warning(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Cannot get event related to the reaction and with event ID {alert_event_id}"
)
return
reacted_to_event = event_response.event
# Only acknowledge reactions to events that we sent
2024-01-22 11:35:13 +01:00
if reacted_to_event.sender not in self.config.user_ids:
return
# Send a message acknowledging the reaction
command = AckAlertCommand(
2022-08-08 00:28:36 +02:00
self.matrix_client,
self.cache,
2022-08-08 00:28:36 +02:00
self.alertmanager_client,
self.config,
room,
event.sender,
2022-07-10 14:49:01 +02:00
event.event_id,
alert_event_id,
2022-07-10 14:49:01 +02:00
)
2022-08-08 12:38:09 +02:00
try:
await command.process()
except (SendRetryError, LocalProtocolError) as e:
2024-01-22 11:35:13 +01:00
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,
)
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,
)
2022-07-10 14:59:21 +02:00
async def redaction(self, room: MatrixRoom, event: RedactionEvent) -> None:
2024-01-22 11:35:13 +01:00
# Ignore message when we aren't the leader in the client pool
if self.matrix_client is not self.matrix_client_pool.matrix_client:
return
2022-07-10 14:59:21 +02:00
# Ignore events from unauthorized room
if room.room_id not in self.config.allowed_rooms:
2022-07-10 14:59:21 +02:00
return
# Ignore redactions from ourselves
2024-01-22 11:35:13 +01:00
if event.sender in self.config.user_ids:
2022-07-10 14:59:21 +02:00
return
2024-01-22 11:35:13 +01:00
logger.debug(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Received event to remove event ID {event.redacts}"
)
2022-07-16 23:20:25 +02:00
command = UnackAlertCommand(
2022-08-08 00:28:36 +02:00
self.matrix_client,
self.cache,
2022-08-08 00:28:36 +02:00
self.alertmanager_client,
self.config,
room,
event.sender,
2022-07-28 10:35:11 +02:00
event.event_id,
event.redacts,
)
2022-08-08 12:38:09 +02:00
try:
await command.process()
except (SendRetryError, LocalProtocolError) as e:
2024-01-22 11:35:13 +01:00
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,
)
2022-07-10 14:59:21 +02:00
2021-01-10 04:30:07 +01:00
async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None:
"""Callback for when an event fails to decrypt. Inform the user.
Args:
room: The room that the event that we were unable to decrypt is in.
event: The encrypted event that we were unable to decrypt.
"""
# Ignore events from unauthorized room
if room.room_id not in self.config.allowed_rooms:
return
2021-01-10 03:58:39 +01:00
logger.error(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Failed to decrypt event!"
2021-01-10 03:58:39 +01:00
f"\n\n"
f"Tip: try using a different device ID in your config file and restart."
f"\n\n"
f"If all else fails, delete your store directory and let the bot recreate "
f"it (your reminders will NOT be deleted, but the bot may respond to existing "
f"commands a second time)."
)
2022-10-26 13:25:47 +02:00
async def key_verification_start(self, event: KeyVerificationStart):
"""Callback for when somebody wants to verify our devices."""
if "emoji" not in event.short_authentication_string:
logger.error(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Cannot use emoji verification with device {event.from_device}."
2022-10-26 13:25:47 +02:00
)
return
event_response = await self.matrix_client.accept_key_verification(
event.transaction_id
)
if isinstance(event_response, ToDeviceError):
logger.error(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Cannot start key verification with device {event.from_device}, got error: {event_response}."
2022-10-26 13:25:47 +02:00
)
return
sas = self.matrix_client.key_verifications[event.transaction_id]
todevice_msg = sas.share_key()
event_response = await self.matrix_client.to_device(todevice_msg)
if isinstance(event_response, ToDeviceError):
logger.error(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Cannot share key with device {event.from_device}, got error: {event_response}."
2022-10-26 13:25:47 +02:00
)
return
async def key_verification_cancel(self, event: KeyVerificationCancel):
# There is no need to issue a
# client.cancel_key_verification(tx_id, reject=False)
# here. The SAS flow is already cancelled.
# We only need to inform the user.
logger.info(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Key verification has been cancelled for reason: {event.reason}."
2022-10-26 13:25:47 +02:00
)
async def key_verification_confirm(self, event: KeyVerificationKey):
2022-10-26 13:25:47 +02:00
sas = self.matrix_client.key_verifications[event.transaction_id]
2022-10-26 17:15:13 +02:00
emoji_list, alt_text_list = zip(*sas.get_emoji())
emoji_str = " ".join(emoji_list)
alt_text_str = " ".join(alt_text_list)
2022-10-26 13:25:47 +02:00
logger.info(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Received request to verify emojis: {emoji_str} ({alt_text_str})"
2022-10-26 13:25:47 +02:00
)
event_response = await self.matrix_client.confirm_short_auth_string(
event.transaction_id
)
if isinstance(event_response, ToDeviceError):
logger.error(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Cannot confirm emoji verification, got error: {event_response}."
2022-10-26 13:25:47 +02:00
)
# FIXME: We should allow manual cancel or reject
# event_response = await self.matrix_client.cancel_key_verification(
# event.transaction_id, reject=True
# )
# if isinstance(event_response, ToDeviceError):
# logger.error(
# f"Unable to reject emoji verification with {event.sender}, got error: {event_response}."
# )
#
# event_response = await self.matrix_client.cancel_key_verification(
# event.transaction_id, reject=False
# )
# if isinstance(event_response, ToDeviceError):
# logger.error(
# f"Unable to cancel emoji verification with {event.sender}, got error: {event_response}."
# )
2024-01-22 11:35:13 +01:00
async def key_verification_end(self, event: KeyVerificationMac) -> None:
2022-10-26 17:15:13 +02:00
try:
sas = self.matrix_client.key_verifications[event.transaction_id]
except KeyError:
logger.error(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Cannot find transaction ID {event.transaction_id}"
2022-10-26 17:15:13 +02:00
)
return
2022-10-26 13:25:47 +02:00
try:
todevice_msg = sas.get_mac()
except LocalProtocolError as e:
# e.g. it might have been cancelled by ourselves
2024-01-22 11:35:13 +01:00
logger.warning(
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Cannot conclude key verification: {e}."
)
2022-10-26 13:25:47 +02:00
return
event_response = await self.matrix_client.to_device(todevice_msg)
if isinstance(event_response, ToDeviceError):
logger.error(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Cannot conclude key verification, got error: {event_response}."
2022-10-26 13:25:47 +02:00
)
return
2022-10-26 14:53:29 +02:00
verified_devices = " ".join(sas.verified_devices)
2022-10-26 13:25:47 +02:00
logger.info(
2024-01-22 11:35:13 +01:00
f"Bot {self.matrix_client.user_id} | Sender {event.sender} | "
f"Successfully verified devices: {verified_devices}"
2022-10-26 13:25:47 +02:00
)
2024-01-22 11:35:13 +01:00
async def unknown_message(
self, room: MatrixRoom, event: RoomMessageUnknown
) -> None:
event_content = event.source["content"]
if event_content["msgtype"] != "m.key.verification.request":
return
2024-01-22 11:35:13 +01:00
if "m.sas.v1" not in event_content["methods"]:
return
2024-01-22 11:35:13 +01:00
response_event = await self.matrix_client.room_send(
room.room_id,
"m.room.message",
{
"msgtype": "m.key.verification.ready",
"methods": ["m.sas.v1"],
"m.relates_to": {"rel_type": "m.reference", "event_id": event.event_id},
},
)
2024-01-22 11:35:13 +01:00
if isinstance(response_event, RoomSendError):
raise SendRetryError(
f"{response_event.status_code} - {response_event.message}"
)