diff --git a/examples/reaction.py b/examples/reaction.py index 204bcd1..e457b4b 100644 --- a/examples/reaction.py +++ b/examples/reaction.py @@ -19,7 +19,8 @@ async def on_message(room: Room, event: Event) -> None: await room.send(event=event, key="hi") if event.body.lower().startswith("❤️"): - await room.send(event=event, message="❤️") + # Or directly reply as a message instead of a reaction + await room.send(message="❤️", event=event) @bot.event @@ -29,15 +30,13 @@ async def on_react(room: Room, event: Event) -> None: and reacts based on the reaction emoji. """ room = bot.get_room(room.room_id) - emoji = event.key - event_id = event.source["content"]["m.relates_to"]["event_id"] if emoji == "🙏": - await room.send(event=event_id, key="❤️") + await room.react(event, "hi") if emoji == "❤️": - await room.send(message="❤️") + await room.react(event, "❤️") bot.start() diff --git a/matrix/context.py b/matrix/context.py index 274d40d..cfcd84a 100644 --- a/matrix/context.py +++ b/matrix/context.py @@ -79,8 +79,14 @@ async def reply(self, message: str) -> None: """ try: + # @todo When Message instance is refactored, refactor this to pass + # the room_id to the message and remove it from `send_message` method. + """example: + c = Message(self.bot, self.room_id) + await c.send_message(message=message) + """ c = Message(self.bot) - await c.send(room_id=self.room_id, message=message) + await c.send_message(room_id=self.room_id, message=message) except Exception as e: raise MatrixError(f"Failed to send message: {e}") diff --git a/matrix/message.py b/matrix/message.py index 2de4366..5f08241 100644 --- a/matrix/message.py +++ b/matrix/message.py @@ -4,7 +4,7 @@ from nio import Event if TYPE_CHECKING: - from .bot import Bot # pragma: no cover + from matrix.bot import Bot # pragma: no cover class Message: @@ -15,15 +15,30 @@ class Message: formatting the message content as either plain text or HTML. :param bot: The bot instance to use for messages. - :type bot: Bot + :param id: The unique identifier of the message event. + :param content: The content of the message. + :param sender: The sender of the message. """ MESSAGE_TYPE = "m.room.message" MATRIX_CUSTOM_HTML = "org.matrix.custom.html" TEXT_MESSAGE_TYPE = "m.text" - def __init__(self, bot: "Bot") -> None: + def __init__( + self, + bot: "Bot", + *, + id: Optional[str] = None, + event: Optional[Event] = None, + content: Optional[str] = None, + sender: Optional[str] = None, + ) -> None: self.bot = bot + self.id = id + if not self.id and event: + self.id = event.event_id + self.content = content + self.sender = sender async def _send_to_room( self, room_id: str, content: Dict, message_type: str = MESSAGE_TYPE @@ -32,11 +47,8 @@ async def _send_to_room( Send a message to the Matrix room. :param room_id: The ID of the room to send the message to. - :type room_id: str :param content: The matrix JSON payload. - :type content: Dict :param message_type: The type of the message. - :type message_type: str :raise MatrixError: If sending the message fails. """ @@ -54,25 +66,20 @@ def _make_content( body: str = "", html: Optional[bool] = None, reaction: Optional[bool] = None, - event_id: Optional[str] = None, key: Optional[str] = None, ) -> Dict: """ Create the content dictionary for a message. :param body: The body of the message. - :type body: str :param html: Wheter to format the message as HTML. - :type html: Optional[bool] :param reaction: Wheter to format the context with a reaction event. - :type reaction: Optional[bool] - :param event_id: The ID of the event to react to. - :type event_id: Optional[str] :param key: The reaction to the message. - :type key: Optional[str] :return: The content of the dictionary. """ + if self.id is None: + raise ValueError("id cannot be None") base: Dict = { "msgtype": self.TEXT_MESSAGE_TYPE, @@ -85,50 +92,67 @@ def _make_content( if reaction: base["m.relates_to"] = { - "event_id": event_id, + "event_id": self.id, "key": key, "rel_type": "m.annotation", } return base - async def send( + async def send_message( self, room_id: str, message: str, format_markdown: Optional[bool] = True ) -> None: """ Send a message to a Matrix room. :param room_id: The ID of the room to send the message to. - :type room_id: str :param message: The message to send. - :type message: str :param format_markdown: Whether to format the message as Markdown (default to True). - :type format_markdown: Optional[bool] """ await self._send_to_room( room_id=room_id, content=self._make_content(body=str(message), html=format_markdown), ) - async def send_reaction(self, room_id: str, event: Event, key: str) -> None: + async def send_reaction(self, room_id: str, key: str) -> None: """ Send a reaction to a message from a user in a Matrix room. :param room_id: The ID of the room to send the message to. - :type room_id: str - :param event: The event object to react to. - :type event: Event :param key: The reaction to the message. - :type key: str """ - if isinstance(event, Event): - event_id = event.event_id - else: - event_id = event - await self._send_to_room( room_id=room_id, - content=self._make_content(event_id=event_id, key=key, reaction=True), + content=self._make_content(key=key, reaction=True), message_type="m.reaction", ) + + @staticmethod + def from_event(bot: "Bot", event: Event) -> "Message": + """ + Method to construct a Message instance from event. + Support regular message events and reaction events. + + :param bot: The bot instance to use for messages. + :param event: The event object to construct the message from. + + :return: The constructed Message instance. + :raise MissingArgumentError: If event is None. + """ + if event is None: + raise ValueError("event cannot be None") + + if isinstance(event, Event) and event.source["type"] == "m.reaction": + event_id = event.source["content"]["m.relates_to"]["event_id"] + body = event.source["content"] + else: + event_id = event.event_id + body = event.body + + return Message( + bot=bot, + id=event_id, + content=body, + sender=event.sender, + ) diff --git a/matrix/room.py b/matrix/room.py index 91e747f..d3142c7 100644 --- a/matrix/room.py +++ b/matrix/room.py @@ -12,9 +12,7 @@ class Room: Represents a Matrix room and provides methods to interact with it. :param room_id: The unique identifier of the room. - :type room_id: str :param bot: The bot instance used to send messages. - :type bot: Bot """ def __init__(self, room_id: str, bot: "Bot") -> None: @@ -24,30 +22,53 @@ def __init__(self, room_id: str, bot: "Bot") -> None: async def send( self, message: str = "", + *, + event: Event, markdown: Optional[bool] = True, - event: Optional[Event] = None, key: Optional[str] = None, ) -> None: """ Send a message to the room. :param message: The message to send. - :type message: str :param markdown: Whether to format the message as Markdown. - :type markdown: Optional[bool] :param event: An event object to react to. - :type event: Optional[Event] :param key: The reaction to the message. - :type key: Optional[str] :raises MatrixError: If sending the message fails. """ try: - msg = Message(self.bot) + msg = self.get_message(self.bot, event) if key: - await msg.send_reaction(self.room_id, event, key) + await msg.send_reaction(self.room_id, key) else: - await msg.send(self.room_id, message, markdown) + await msg.send_message(self.room_id, message, markdown) + except Exception as e: + raise MatrixError(f"Failed to send message: {e}") + + @staticmethod + def get_message(bot: "Bot", event: Event) -> Message: + """ + Get a Message instance from an event. + :param bot: The bot instance to use for messages. + :param event: The event object to construct the message from. + + :return: The constructed Message instance. + """ + if not event and not bot: + raise MatrixError("Failed to get message.") + + return Message.from_event(bot, event) + + async def react(self, event: Event, key: str) -> None: + """ + Send a reaction to a message in the room. + + :param event: The event to react to. + :param key: The reaction to the message. + """ + try: + await self.send(event=event, key=key) except Exception as e: raise MatrixError(f"Failed to send message: {e}") @@ -70,9 +91,7 @@ async def ban_user(self, user_id: str, reason: Optional[str] = None) -> None: Ban a user from a room. :param user_id: The ID of the user to ban of the room. - :type user_id: str :param reason: The reason to ban the user. - :type reason: Optional[str] :raises MatrixError: If banning the user fails. """ @@ -89,7 +108,6 @@ async def unban_user(self, user_id: str) -> None: Unban a user from a room. :param user_id: The ID of the user to unban of the room. - :type user_id: str :raises MatrixError: If unbanning the user fails. """ @@ -104,9 +122,7 @@ async def kick_user(self, user_id: str, reason: Optional[str] = None) -> None: Kick a user from a room. :param user_id: The ID of the user to kick of the room. - :type user_id: str :param reason: The reason to kick the user. - :type reason: Optional[str] :raises MatrixError: If kicking the user fails. """ diff --git a/tests/test_message.py b/tests/test_message.py index ac39a7e..212f21f 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -36,7 +36,7 @@ async def test_send_message_success(message_default): room_id = "!room:id" message = "Hello, world!" - await message_default.send(room_id, message) + await message_default.send_message(room_id, message) message_default.bot.client.room_send.assert_awaited_once() @@ -49,7 +49,7 @@ async def test_send_message_failure(message_default): "Failed to send message" ) with pytest.raises(MatrixError, match="Failed to send message"): - await message_default.send(room_id, message) + await message_default.send_message(room_id, message) def test_make_content_with_html(message_default): @@ -76,7 +76,7 @@ def test_make_content_without_html(message_default): async def test_send_reaction_success(message_default, event): room_id = "!room:id" - await message_default.send_reaction(room_id, event, "hi") + await message_default.send_reaction(room_id, "hi") message_default.bot.client.room_send.assert_awaited_once() @@ -88,4 +88,4 @@ async def test_send_reaction_failure(message_default, event): "Failed to send message" ) with pytest.raises(MatrixError, match="Failed to send message"): - await message_default.send_reaction(room_id, event, "🙏") + await message_default.send_reaction(room_id, "🙏")