diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc841e1..7db88c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * Added option to not bridge chats with lots of members. * Added option to include captions in the same message as the media to implement [MSC2530]. +* Added support for bridging forwarded messages as forwards on Telegram. + * If forwarding fails (e.g. due to it being blocked in the source chat), the + bridge will automatically fall back to sending it as a normal new message. * Added options to make encryption more secure. * The `encryption` -> `verification_levels` config options can be used to make the bridge require encrypted messages to come from cross-signed diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index 85af29a8..d51ad24b 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -1897,6 +1897,68 @@ class Portal(DBPortal, BasePortal): message_type=content.msgtype, ) + async def _find_source_msg( + self, sender: u.User, content: MessageEventContent + ) -> DBMessage | None: + try: + source = content["fi.mau.telegram.source"] + except KeyError: + return None + if not isinstance(source, dict): + return None + try: + msg_id = source["id"] + space = source["space"] + chat_id = source["chat_id"] + peer_type = source["peer_type"] + except KeyError: + return None + if ( + not isinstance(msg_id, int) + or not isinstance(chat_id, int) + or not isinstance(space, int) + or not isinstance(peer_type, str) + ): + return None + elif await sender.needs_relaybot(self): + return None + if peer_type == "user" and space != sender.tgid: + return + dbm = await DBMessage.get_one_by_tgid(TelegramID(msg_id), TelegramID(space)) + if dbm and peer_type == "chat" and space != sender.tgid: + dbm = DBMessage.get_by_mxid(dbm.mxid, dbm.mx_room, sender.tgid) + return dbm + + async def _handle_matrix_forward( + self, + sender: u.User, + msg: DBMessage, + event_id: EventID, + space: TelegramID, + msgtype: MessageType, + ) -> bool: + source_portal = await Portal.get_by_mxid(msg.mx_room) + if not source_portal: + return False + async with self.send_lock(sender.tgid): + try: + response = await sender.client.forward_messages( + self.peer, + messages=[msg.tgid], + from_peer=source_portal.peer, + ) + except Exception as e: + self.log.warning( + f"Failed to send {event_id} from {sender.mxid} as forward of {msg.tgid} " + f"from {source_portal.tgid}: {e}, falling back to normal message handling" + ) + return False + else: + await self._mark_matrix_handled( + sender, EventType.ROOM_MESSAGE, event_id, space, 0, response[0], msgtype + ) + return True + async def _handle_matrix_message( self, sender: u.User, content: MessageEventContent, event_id: EventID ) -> None: @@ -1912,6 +1974,11 @@ class Portal(DBPortal, BasePortal): if self.peer_type == "channel" # Channels have their own ID space else (sender.tgid if logged_in else self.bot.tgid) ) + source_msg = await self._find_source_msg(sender, content) + if source_msg and await self._handle_matrix_forward( + sender, source_msg, event_id, space, content.msgtype + ): + return reply_to = await formatter.matrix_reply_to_telegram(content, space, room_id=self.mxid) media = ( diff --git a/mautrix_telegram/portal_util/message_convert.py b/mautrix_telegram/portal_util/message_convert.py index 23950d01..23a1c238 100644 --- a/mautrix_telegram/portal_util/message_convert.py +++ b/mautrix_telegram/portal_util/message_convert.py @@ -159,7 +159,16 @@ class TelegramMessageConverter: return if converted: converted.content.external_url = self._get_external_url(evt) + converted.content["fi.mau.telegram.source"] = { + "space": self.portal.tgid if self.portal.peer_type == "channel" else source.tgid, + "chat_id": self.portal.tgid, + "peer_type": self.portal.peer_type, + "id": evt.id, + } if converted.caption: + converted.caption["fi.mau.telegram.source"] = converted.content[ + "fi.mau.telegram.source" + ] converted.caption.external_url = converted.content.external_url if self.portal.get_config("caption_in_message"): self._caption_to_message(converted)