2020-08-10 00:02:07 +02:00
|
|
|
import logging
|
|
|
|
|
2022-07-06 00:54:13 +02:00
|
|
|
from diskcache import Cache
|
2021-01-10 04:30:07 +01:00
|
|
|
from nio import (
|
|
|
|
AsyncClient,
|
|
|
|
InviteMemberEvent,
|
|
|
|
JoinError,
|
|
|
|
MatrixRoom,
|
|
|
|
MegolmEvent,
|
2022-07-10 14:42:23 +02:00
|
|
|
RedactionEvent,
|
2021-01-10 04:30:07 +01:00
|
|
|
RoomGetEventError,
|
|
|
|
RoomMessageText,
|
|
|
|
UnknownEvent,
|
|
|
|
)
|
2020-08-10 00:02:07 +02:00
|
|
|
|
2022-07-04 01:03:24 +02:00
|
|
|
from matrix_alertbot.alertmanager import AlertmanagerClient
|
2022-07-10 13:13:32 +02:00
|
|
|
from matrix_alertbot.chat_functions import strip_fallback
|
2022-07-16 23:08:12 +02:00
|
|
|
from matrix_alertbot.command import CommandFactory
|
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__)
|
|
|
|
|
|
|
|
|
2022-07-10 13:13:32 +02:00
|
|
|
REACTION_DURATIONS = {"🤫": "12h", "😶": "1d", "🤐": "3d", "🙊": "5d", "🔇": "1w", "🔕": "3w"}
|
|
|
|
|
|
|
|
|
2021-01-10 04:30:07 +01:00
|
|
|
class Callbacks:
|
2022-07-04 01:03:24 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
client: AsyncClient,
|
|
|
|
alertmanager: AlertmanagerClient,
|
2022-07-08 21:11:25 +02:00
|
|
|
cache: Cache,
|
2022-07-04 01:03:24 +02:00
|
|
|
config: Config,
|
|
|
|
):
|
2019-09-25 14:26:29 +02:00
|
|
|
"""
|
|
|
|
Args:
|
2021-01-10 04:33:59 +01:00
|
|
|
client: nio client used to interact with matrix.
|
2019-09-25 14:26:29 +02:00
|
|
|
|
2022-07-04 01:03:24 +02:00
|
|
|
cache: Bot cache.
|
|
|
|
|
|
|
|
alertmanager: Client used to interact with alertmanager.
|
2019-09-25 14:26:29 +02:00
|
|
|
|
2021-01-10 04:33:59 +01:00
|
|
|
config: Bot configuration parameters.
|
2019-09-25 14:26:29 +02:00
|
|
|
"""
|
|
|
|
self.client = client
|
2022-07-08 21:11:25 +02:00
|
|
|
self.cache = cache
|
2022-07-04 01:03:24 +02:00
|
|
|
self.alertmanager = alertmanager
|
2019-10-04 15:44:19 +02:00
|
|
|
self.config = config
|
|
|
|
self.command_prefix = config.command_prefix
|
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:
|
2021-01-10 04:33:59 +01:00
|
|
|
room: The room the event came from.
|
2019-09-25 14:26:29 +02:00
|
|
|
|
2021-01-10 04:33:59 +01:00
|
|
|
event: The event defining the message.
|
2019-09-25 14:26:29 +02:00
|
|
|
"""
|
|
|
|
# Extract the message text
|
2022-07-04 01:03:24 +02:00
|
|
|
msg = strip_fallback(event.body)
|
2019-09-25 14:26:29 +02:00
|
|
|
|
|
|
|
# Ignore messages from ourselves
|
|
|
|
if event.sender == self.client.user:
|
|
|
|
return
|
|
|
|
|
2022-07-04 01:03:24 +02:00
|
|
|
# Ignore messages from unauthorized room
|
2022-07-08 21:11:25 +02:00
|
|
|
if room.room_id != self.config.room_id:
|
2022-07-04 01:03:24 +02:00
|
|
|
return
|
|
|
|
|
2019-09-25 14:26:29 +02:00
|
|
|
logger.debug(
|
|
|
|
f"Bot message received for room {room.display_name} | "
|
|
|
|
f"{room.user_name(event.sender)}: {msg}"
|
|
|
|
)
|
2019-10-04 15:44:19 +02:00
|
|
|
# Process as message if in a public room without command prefix
|
2019-09-25 14:26:29 +02:00
|
|
|
has_command_prefix = msg.startswith(self.command_prefix)
|
2022-07-04 01:03:24 +02:00
|
|
|
if not has_command_prefix:
|
|
|
|
logger.debug(
|
|
|
|
f"Message received without command prefix {self.command_prefix}: Aborting."
|
|
|
|
)
|
2019-09-25 14:26:29 +02:00
|
|
|
return
|
|
|
|
|
2022-07-10 14:06:36 +02:00
|
|
|
source_content = event.source["content"]
|
2022-07-16 23:08:12 +02:00
|
|
|
alert_event_id = (
|
|
|
|
source_content.get("m.relates_to", {})
|
|
|
|
.get("m.in_reply_to", {})
|
|
|
|
.get("event_id")
|
|
|
|
)
|
|
|
|
|
|
|
|
if alert_event_id is None:
|
|
|
|
logger.warning("Unable to find the event ID of the alert")
|
2022-07-10 14:06:36 +02:00
|
|
|
|
2022-07-04 01:03:24 +02:00
|
|
|
# Remove the command prefix
|
2022-07-16 23:08:12 +02:00
|
|
|
cmd = msg[len(self.command_prefix) :]
|
|
|
|
command = CommandFactory.create(
|
|
|
|
cmd,
|
2022-07-10 14:06:36 +02:00
|
|
|
self.client,
|
|
|
|
self.cache,
|
|
|
|
self.alertmanager,
|
|
|
|
self.config,
|
|
|
|
room,
|
|
|
|
event.sender,
|
2022-07-16 23:08:12 +02:00
|
|
|
event.event_id,
|
2022-07-10 14:06:36 +02:00
|
|
|
alert_event_id,
|
2022-07-04 01:03:24 +02:00
|
|
|
)
|
2019-09-25 14:26:29 +02:00
|
|
|
await command.process()
|
|
|
|
|
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.
|
|
|
|
"""
|
2022-07-04 01:03:24 +02:00
|
|
|
# Ignore invites from unauthorized room
|
2022-07-08 21:11:25 +02:00
|
|
|
if room.room_id != self.config.room_id:
|
2022-07-04 01:03:24 +02:00
|
|
|
return
|
|
|
|
|
2019-09-25 14:26:29 +02:00
|
|
|
logger.debug(f"Got invite to {room.room_id} from {event.sender}.")
|
|
|
|
|
|
|
|
# Attempt to join 3 times before giving up
|
|
|
|
for attempt in range(3):
|
|
|
|
result = await self.client.join(room.room_id)
|
|
|
|
if type(result) == JoinError:
|
|
|
|
logger.error(
|
|
|
|
f"Error joining room {room.room_id} (attempt %d): %s",
|
2020-08-10 00:02:07 +02:00
|
|
|
attempt,
|
|
|
|
result.message,
|
2019-09-25 14:26:29 +02:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
break
|
2020-06-23 03:11:31 +02:00
|
|
|
else:
|
|
|
|
logger.error("Unable to join room: %s", room.room_id)
|
2019-09-25 14:26:29 +02:00
|
|
|
|
2020-06-23 03:11:31 +02:00
|
|
|
# Successfully joined room
|
|
|
|
logger.info(f"Joined {room.room_id}")
|
2021-01-04 05:47:27 +01:00
|
|
|
|
2021-10-25 21:28:38 +02:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
if event.state_key == self.client.user_id:
|
|
|
|
# This is our own membership (invite) event
|
|
|
|
await self.invite(room, event)
|
|
|
|
|
2021-01-04 05:47:27 +01:00
|
|
|
async def _reaction(
|
2022-07-16 23:08:12 +02:00
|
|
|
self, room: MatrixRoom, event: UnknownEvent, alert_event_id: str
|
2021-01-10 04:30:07 +01:00
|
|
|
) -> None:
|
2021-01-04 05:47:27 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2022-07-04 01:03:24 +02:00
|
|
|
# Ignore reactions from unauthorized room
|
2022-07-08 21:11:25 +02:00
|
|
|
if room.room_id != self.config.room_id:
|
2022-07-04 01:03:24 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
# Ignore reactions from ourselves
|
|
|
|
if event.sender == self.client.user:
|
|
|
|
return
|
|
|
|
|
2022-07-10 13:13:32 +02:00
|
|
|
reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key")
|
|
|
|
logger.debug(f"Got reaction {reaction} to {room.room_id} from {event.sender}.")
|
|
|
|
|
|
|
|
if reaction not in REACTION_DURATIONS:
|
|
|
|
logger.warning(f"Uknown duration reaction {reaction}")
|
|
|
|
return
|
|
|
|
duration = REACTION_DURATIONS[reaction]
|
2021-01-04 05:47:27 +01:00
|
|
|
|
|
|
|
# Get the original event that was reacted to
|
2022-07-16 23:08:12 +02:00
|
|
|
event_response = await self.client.room_get_event(room.room_id, alert_event_id)
|
2021-01-04 05:47:27 +01:00
|
|
|
if isinstance(event_response, RoomGetEventError):
|
2022-07-16 23:08:12 +02:00
|
|
|
logger.warning(
|
|
|
|
f"Error getting event that was reacted to ({alert_event_id})"
|
|
|
|
)
|
2021-01-04 05:47:27 +01:00
|
|
|
return
|
|
|
|
reacted_to_event = event_response.event
|
|
|
|
|
|
|
|
# Only acknowledge reactions to events that we sent
|
|
|
|
if reacted_to_event.sender != self.config.user_id:
|
|
|
|
return
|
|
|
|
|
2022-07-10 14:49:01 +02:00
|
|
|
self.cache.set(
|
|
|
|
event.event_id,
|
|
|
|
reacted_to_event.event_id,
|
|
|
|
expire=self.config.cache_expire_time,
|
|
|
|
)
|
|
|
|
|
2021-01-04 05:47:27 +01:00
|
|
|
# Send a message acknowledging the reaction
|
2022-07-16 23:08:12 +02:00
|
|
|
command = CommandFactory.create(
|
|
|
|
f"ack {duration}",
|
2021-01-04 05:47:27 +01:00
|
|
|
self.client,
|
2022-07-10 13:13:32 +02:00
|
|
|
self.cache,
|
|
|
|
self.alertmanager,
|
|
|
|
self.config,
|
|
|
|
room,
|
2022-07-10 14:06:36 +02:00
|
|
|
event.sender,
|
2022-07-16 23:08:12 +02:00
|
|
|
event.event_id,
|
|
|
|
alert_event_id,
|
2021-01-04 05:47:27 +01:00
|
|
|
)
|
2022-07-10 13:13:32 +02:00
|
|
|
await command.process()
|
2021-01-04 05:47:27 +01:00
|
|
|
|
2022-07-10 14:59:21 +02:00
|
|
|
async def redaction(self, room: MatrixRoom, event: RedactionEvent) -> None:
|
|
|
|
# Ignore events from unauthorized room
|
|
|
|
if room.room_id != self.config.room_id:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Ignore redactions from ourselves
|
|
|
|
if event.sender == self.config.user_id:
|
|
|
|
return
|
|
|
|
|
2022-07-10 15:11:25 +02:00
|
|
|
if event.redacts not in self.cache:
|
|
|
|
logger.warning(f"Error removing silence from reaction {event.redacts}")
|
|
|
|
return
|
|
|
|
|
2022-07-16 23:08:12 +02:00
|
|
|
alert_event_id = self.cache[event.redacts]
|
2022-07-10 14:59:21 +02:00
|
|
|
|
2022-07-16 23:08:12 +02:00
|
|
|
command = CommandFactory.create(
|
|
|
|
"unack",
|
2022-07-10 14:59:21 +02:00
|
|
|
self.client,
|
|
|
|
self.cache,
|
|
|
|
self.alertmanager,
|
|
|
|
self.config,
|
|
|
|
room,
|
|
|
|
event.sender,
|
2022-07-16 23:08:12 +02:00
|
|
|
event.redacts,
|
|
|
|
alert_event_id,
|
2022-07-10 14:59:21 +02:00
|
|
|
)
|
|
|
|
await command.process()
|
|
|
|
|
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.
|
|
|
|
"""
|
2022-07-04 01:03:24 +02:00
|
|
|
# Ignore events from unauthorized room
|
2022-07-08 21:11:25 +02:00
|
|
|
if room.room_id != self.config.room_id:
|
2022-07-04 01:03:24 +02:00
|
|
|
return
|
|
|
|
|
2021-01-10 03:58:39 +01:00
|
|
|
logger.error(
|
|
|
|
f"Failed to decrypt event '{event.event_id}' in room '{room.room_id}'!"
|
|
|
|
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)."
|
|
|
|
)
|
|
|
|
|
2021-01-10 04:30:07 +01:00
|
|
|
async def unknown(self, room: MatrixRoom, event: UnknownEvent) -> None:
|
2021-01-04 05:47:27 +01:00
|
|
|
"""Callback for when an event with a type that is unknown to matrix-nio is received.
|
2021-01-10 04:30:07 +01:00
|
|
|
Currently this is used for reaction events, which are not yet part of a released
|
|
|
|
matrix spec (and are thus unknown to nio).
|
2021-01-04 05:47:27 +01:00
|
|
|
|
|
|
|
Args:
|
|
|
|
room: The room the reaction was sent in.
|
|
|
|
|
2021-01-10 04:30:07 +01:00
|
|
|
event: The event itself.
|
2021-01-04 05:47:27 +01:00
|
|
|
"""
|
2022-07-04 01:03:24 +02:00
|
|
|
# Ignore events from unauthorized room
|
2022-07-08 21:11:25 +02:00
|
|
|
if room.room_id != self.config.room_id:
|
2022-07-04 01:03:24 +02:00
|
|
|
return
|
|
|
|
|
2021-01-04 05:47:27 +01:00
|
|
|
if event.type == "m.reaction":
|
|
|
|
# Get the ID of the event this was a reaction to
|
|
|
|
relation_dict = event.source.get("content", {}).get("m.relates_to", {})
|
|
|
|
|
2022-07-10 14:06:36 +02:00
|
|
|
reacted_to_id = relation_dict.get("event_id")
|
|
|
|
if reacted_to_id and relation_dict.get("rel_type") == "m.annotation":
|
|
|
|
await self._reaction(room, event, reacted_to_id)
|
2021-01-10 04:30:07 +01:00
|
|
|
return
|
2021-01-04 05:47:27 +01:00
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
f"Got unknown event with type to {event.type} from {event.sender} in {room.room_id}."
|
|
|
|
)
|