From 7d998dca3f516abdd50398d5a57e922fd21c3426 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jun 2022 15:36:22 +0300 Subject: [PATCH] Add support for custom message bridging status events --- mautrix_telegram/config.py | 1 + mautrix_telegram/example-config.yaml | 2 + mautrix_telegram/portal.py | 70 ++++++++++++++++++++++------ requirements.txt | 2 +- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 6f9badcf..66977545 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -156,6 +156,7 @@ class Config(BaseBridgeConfig): copy("bridge.private_chat_portal_meta") copy("bridge.delivery_receipts") copy("bridge.delivery_error_reports") + copy("bridge.message_status_events") copy("bridge.resend_bridge_info") copy("bridge.mute_bridging") copy("bridge.pinned_tag") diff --git a/mautrix_telegram/example-config.yaml b/mautrix_telegram/example-config.yaml index ff7edf74..4e9d747b 100644 --- a/mautrix_telegram/example-config.yaml +++ b/mautrix_telegram/example-config.yaml @@ -265,6 +265,8 @@ bridge: delivery_receipts: false # Whether or not delivery errors should be reported as messages in the Matrix room. delivery_error_reports: false + # Whether the bridge should send the message status as a custom com.beeper.message_send_status event. + message_status_events: false # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. # This field will automatically be changed back to false after it, # except if the config file is not writable. diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index 858e8bc2..229594ec 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -133,6 +133,7 @@ from mautrix.bridge import BasePortal, NotificationDisabler, RejectMatrixInvite, from mautrix.errors import IntentError, MatrixRequestError, MForbidden from mautrix.types import ( BatchID, + BeeperMessageStatusEventContent, ContentURI, EventID, EventType, @@ -143,9 +144,11 @@ from mautrix.types import ( MediaMessageEventContent, Membership, MessageEventContent, + MessageStatusReason, MessageType, PowerLevelStateEventContent, RelatesTo, + RelationType, RoomAlias, RoomAvatarStateEventContent, RoomCreatePreset, @@ -205,6 +208,10 @@ class BridgingError(Exception): pass +class IgnoredMessageError(Exception): + pass + + class Portal(DBPortal, BasePortal): bot: "Bot" config: Config @@ -1787,6 +1794,31 @@ class Portal(DBPortal, BasePortal): message_type=msgtype, ) await self._send_delivery_receipt(event_id) + asyncio.create_task(self._send_message_status(event_id, err=None)) + + async def _send_message_status(self, event_id: EventID, err: Exception | None) -> None: + if not self.config["bridge.message_status_events"]: + return + intent = self.az.intent if self.encrypted else self.main_intent + status = BeeperMessageStatusEventContent( + network=self.bridge_info_state_key, + relates_to=RelatesTo( + rel_type=RelationType.REFERENCE, + event_id=event_id, + ), + success=err is None, + ) + if err: + status.reason = MessageStatusReason.GENERIC_ERROR + status.error = str(err) + status.is_certain = True + status.can_retry = not isinstance(err, IgnoredMessageError) + + await intent.send_message_event( + room_id=self.mxid, + event_type=EventType.BEEPER_MESSAGE_STATUS, + content=status, + ) async def _send_bridge_error( self, @@ -1810,6 +1842,7 @@ class Portal(DBPortal, BasePortal): await self._send_message( self.main_intent, TextMessageEventContent(msgtype=MessageType.NOTICE, body=msg) ) + await self._send_message_status(event_id, err) async def handle_matrix_message( self, sender: u.User, content: MessageEventContent, event_id: EventID @@ -1840,9 +1873,10 @@ class Portal(DBPortal, BasePortal): async def _handle_matrix_message( self, sender: u.User, content: MessageEventContent, event_id: EventID ) -> None: - if not content.body or not content.msgtype: - self.log.debug(f"Ignoring message {event_id} in {self.mxid} without body or msgtype") - return + if not content.msgtype: + raise IgnoredMessageError("Message doesn't have a msgtype") + elif not content.body: + raise IgnoredMessageError("Message doesn't have a body") logged_in = not await sender.needs_relaybot(self) client = sender.client if logged_in else self.bot.client @@ -1865,7 +1899,7 @@ class Portal(DBPortal, BasePortal): bridge_notices = self.get_config("bridge_notices.default") excepted = sender.mxid in self.get_config("bridge_notices.exceptions") if not bridge_notices and not excepted: - raise BridgingError("Notices are not configured to be bridged.") + raise IgnoredMessageError("Notices are not configured to be bridged.") if content.msgtype in (MessageType.TEXT, MessageType.EMOTE, MessageType.NOTICE): await self._pre_process_matrix_message(sender, not logged_in, content) @@ -1898,7 +1932,7 @@ class Portal(DBPortal, BasePortal): f"Didn't handle Matrix event {event_id} due to unknown msgtype {content.msgtype}" ) self.log.trace("Unhandled Matrix event content: %s", content) - raise BridgingError(f"Unhandled msgtype {content.msgtype}") + raise IgnoredMessageError(f"Unhandled msgtype {content.msgtype}") async def handle_matrix_unpin_all(self, sender: u.User, pin_event_id: EventID) -> None: await sender.client(UnpinAllMessagesRequest(peer=self.peer)) @@ -1928,7 +1962,7 @@ class Portal(DBPortal, BasePortal): ) -> None: try: await self._handle_matrix_deletion(deleter, event_id) - except BridgingError as e: + except IgnoredMessageError as e: self.log.debug(str(e)) await self._send_bridge_error(deleter, e, redaction_event_id, EventType.ROOM_REDACTION) except Exception as e: @@ -1942,20 +1976,21 @@ class Portal(DBPortal, BasePortal): EventType.ROOM_REDACTION, ) await self._send_delivery_receipt(redaction_event_id) + asyncio.create_task(self._send_message_status(redaction_event_id, err=None)) async def _handle_matrix_reaction_deletion( self, deleter: u.User, event_id: EventID, tg_space: TelegramID ) -> None: reaction = await DBReaction.get_by_mxid(event_id, self.mxid) if not reaction: - raise BridgingError(f"Ignoring Matrix redaction of unknown event {event_id}") + raise IgnoredMessageError(f"Ignoring Matrix redaction of unknown event {event_id}") elif reaction.tg_sender != deleter.tgid: - raise BridgingError(f"Ignoring Matrix redaction of reaction by another user") + raise IgnoredMessageError(f"Ignoring Matrix redaction of reaction by another user") reaction_target = await DBMessage.get_by_mxid( reaction.msg_mxid, reaction.mx_room, tg_space ) if not reaction_target or reaction_target.redacted: - raise BridgingError( + raise IgnoredMessageError( f"Ignoring Matrix redaction of reaction to unknown event {reaction.msg_mxid}" ) async with self.reaction_lock(reaction_target.mxid): @@ -1969,13 +2004,13 @@ class Portal(DBPortal, BasePortal): if not message: await self._handle_matrix_reaction_deletion(real_deleter, event_id, tg_space) elif message.redacted: - raise BridgingError( + raise IgnoredMessageError( "Ignoring Matrix redaction of already redacted event " f"{message.mxid} in {message.mx_room}" ) elif message.edit_index != 0: await message.mark_redacted() - raise BridgingError( + raise IgnoredMessageError( f"Ignoring Matrix redaction of edit event {message.mxid} in {message.mx_room}" ) else: @@ -1990,7 +2025,7 @@ class Portal(DBPortal, BasePortal): await self._handle_matrix_reaction( user, target_event_id, reaction, reaction_event_id ) - except BridgingError as e: + except IgnoredMessageError as e: self.log.debug(str(e)) await self._send_bridge_error(user, e, reaction_event_id, EventType.REACTION) except ReactionInvalidError as e: @@ -2012,6 +2047,7 @@ class Portal(DBPortal, BasePortal): EventType.REACTION, ) await self._send_delivery_receipt(reaction_event_id) + asyncio.create_task(self._send_message_status(reaction_event_id, err=None)) async def _handle_matrix_reaction( self, user: u.User, target_event_id: EventID, emoji: str, reaction_event_id: EventID @@ -2019,11 +2055,15 @@ class Portal(DBPortal, BasePortal): tg_space = self.tgid if self.peer_type == "channel" else user.tgid msg = await DBMessage.get_by_mxid(target_event_id, self.mxid, tg_space) if not msg: - raise BridgingError(f"Ignoring Matrix reaction to unknown event {target_event_id}") + raise IgnoredMessageError( + f"Ignoring Matrix reaction to unknown event {target_event_id}" + ) elif msg.redacted: - raise BridgingError(f"Ignoring Matrix reaction to redacted event {target_event_id}") + raise IgnoredMessageError( + f"Ignoring Matrix reaction to redacted event {target_event_id}" + ) elif msg.edit_index != 0: - raise BridgingError(f"Ignoring Matrix reaction to edit event {target_event_id}") + raise IgnoredMessageError(f"Ignoring Matrix reaction to edit event {target_event_id}") emoji = variation_selector.remove(emoji) existing_react = await DBReaction.get_by_sender(msg.mxid, msg.mx_room, user.tgid) diff --git a/requirements.txt b/requirements.txt index e1d8a480..83244720 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 yarl>=1,<2 -mautrix>=0.16.5,<0.17 +mautrix>=0.16.6,<0.17 #telethon>=1.24,<1.25 tulir-telethon==1.25.0a15 asyncpg>=0.20,<0.26