From 3f6a4237ad28c23269b1aa1ec3fe9bdc1fbc212d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 25 May 2020 13:25:37 +0300 Subject: [PATCH] Add option to send read receipt on confirmed delivery to Telegram --- mautrix_telegram/config.py | 35 ++++++-------------------- mautrix_telegram/example-config.yaml | 3 +++ mautrix_telegram/matrix.py | 30 +++++++++++++--------- mautrix_telegram/portal/base.py | 6 +++-- mautrix_telegram/portal/matrix.py | 37 ++++++++++++++++++++++------ mautrix_telegram/portal/metadata.py | 2 +- 6 files changed, 63 insertions(+), 50 deletions(-) diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index f69cc5c4..e50e8ce5 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -45,26 +45,22 @@ class Config(BaseBridgeConfig): ] def do_update(self, helper: ConfigUpdateHelper) -> None: + super().do_update(helper) copy, copy_dict, base = helper - copy("homeserver.address") - copy("homeserver.domain") - copy("homeserver.verify_ssl") - if "appservice.protocol" in self and "appservice.address" not in self: protocol, hostname, port = (self["appservice.protocol"], self["appservice.hostname"], self["appservice.port"]) base["appservice.address"] = f"{protocol}://{hostname}:{port}" - else: - copy("appservice.address") + if "appservice.debug" in self and "logging" not in self: + level = "DEBUG" if self["appservice.debug"] else "INFO" + base["logging.root.level"] = level + base["logging.loggers.mau.level"] = level + base["logging.loggers.telethon.level"] = level + # TODO move these to mautrix-python copy("appservice.tls_cert") copy("appservice.tls_key") - copy("appservice.hostname") - copy("appservice.port") - copy("appservice.max_body_size") - - copy("appservice.database") copy("appservice.public.enabled") copy("appservice.public.prefix") @@ -76,16 +72,8 @@ class Config(BaseBridgeConfig): if base["appservice.provisioning.shared_secret"] == "generate": base["appservice.provisioning.shared_secret"] = self._new_token() - copy("appservice.id") - copy("appservice.bot_username") - copy("appservice.bot_displayname") - copy("appservice.bot_avatar") - copy("appservice.community_id") - copy("appservice.as_token") - copy("appservice.hs_token") - copy("metrics.enabled") copy("metrics.listen_port") @@ -124,6 +112,7 @@ class Config(BaseBridgeConfig): copy("bridge.encryption.allow") copy("bridge.encryption.default") copy("bridge.private_chat_portal_meta") + copy("bridge.delivery_receipts") copy("bridge.initial_power_level_overrides.group") copy("bridge.initial_power_level_overrides.user") @@ -208,14 +197,6 @@ class Config(BaseBridgeConfig): copy("telegram.proxy.username") copy("telegram.proxy.password") - if "appservice.debug" in self and "logging" not in self: - level = "DEBUG" if self["appservice.debug"] else "INFO" - base["logging.root.level"] = level - base["logging.loggers.mau.level"] = level - base["logging.loggers.telethon.level"] = level - else: - copy("logging") - def _get_permissions(self, key: str) -> Permissions: level = self["bridge.permissions"].get(key, "") admin = level == "admin" diff --git a/mautrix_telegram/example-config.yaml b/mautrix_telegram/example-config.yaml index 110d4189..4981dee6 100644 --- a/mautrix_telegram/example-config.yaml +++ b/mautrix_telegram/example-config.yaml @@ -210,6 +210,9 @@ bridge: # Whether or not to explicitly set the avatar and room name for private # chat portal rooms. This will be implicitly enabled if encryption.default is true. private_chat_portal_meta: false + # Whether or not the bridge should send a read receipt from the bridge bot when a message has + # been sent to Telegram. + delivery_receipts: false # Overrides for base power levels. initial_power_level_overrides: diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 6d600669..c6873cfb 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -278,7 +278,7 @@ class MatrixHandler(BaseMatrixHandler): if not portal: return - await portal.handle_matrix_deletion(sender, evt.redacts) + await portal.handle_matrix_deletion(sender, evt.redacts, evt.event_id) @staticmethod async def handle_power_levels(evt: StateEvent) -> None: @@ -286,11 +286,12 @@ class MatrixHandler(BaseMatrixHandler): sender = await u.User.get_by_mxid(evt.sender).ensure_started() if await sender.has_full_access(allow_bot=True) and portal: await portal.handle_matrix_power_levels(sender, evt.content.users, - evt.unsigned.prev_content.users) + evt.unsigned.prev_content.users, + evt.event_id) @staticmethod async def handle_room_meta(evt_type: EventType, room_id: RoomID, sender_mxid: UserID, - content: RoomMetaStateEventContent) -> None: + content: RoomMetaStateEventContent, event_id: EventID) -> None: portal = po.Portal.get_by_mxid(room_id) sender = await u.User.get_by_mxid(sender_mxid).ensure_started() if await sender.has_full_access(allow_bot=True) and portal: @@ -301,27 +302,29 @@ class MatrixHandler(BaseMatrixHandler): }[evt_type] if not isinstance(content, content_type): return - await handler(sender, content[content_key]) + await handler(sender, content[content_key], event_id) @staticmethod async def handle_room_pin(room_id: RoomID, sender_mxid: UserID, - new_events: Set[str], old_events: Set[str]) -> None: + new_events: Set[str], old_events: Set[str], + event_id: EventID) -> None: portal = po.Portal.get_by_mxid(room_id) sender = await u.User.get_by_mxid(sender_mxid).ensure_started() if await sender.has_full_access(allow_bot=True) and portal: events = new_events - old_events if len(events) > 0: # New event pinned, set that as pinned in Telegram. - await portal.handle_matrix_pin(sender, EventID(events.pop())) + await portal.handle_matrix_pin(sender, EventID(events.pop()), event_id) elif len(new_events) == 0: # All pinned events removed, remove pinned event in Telegram. - await portal.handle_matrix_pin(sender, None) + await portal.handle_matrix_pin(sender, None, event_id) @staticmethod - async def handle_room_upgrade(room_id: RoomID, sender: UserID, new_room_id: RoomID) -> None: + async def handle_room_upgrade(room_id: RoomID, sender: UserID, new_room_id: RoomID, + event_id: EventID) -> None: portal = po.Portal.get_by_mxid(room_id) if portal: - await portal.handle_matrix_upgrade(sender, new_room_id) + await portal.handle_matrix_upgrade(sender, new_room_id, event_id) async def handle_member_info_change(self, room_id: RoomID, user_id: UserID, profile: MemberStateEventContent, @@ -409,16 +412,19 @@ class MatrixHandler(BaseMatrixHandler): if evt.type == EventType.ROOM_POWER_LEVELS: await self.handle_power_levels(evt) elif evt.type in (EventType.ROOM_NAME, EventType.ROOM_AVATAR, EventType.ROOM_TOPIC): - await self.handle_room_meta(evt.type, evt.room_id, evt.sender, evt.content) + await self.handle_room_meta(evt.type, evt.room_id, evt.sender, evt.content, + evt.event_id) elif evt.type == EventType.ROOM_PINNED_EVENTS: new_events = set(evt.content.pinned) try: old_events = set(evt.unsigned.prev_content.pinned) except (KeyError, ValueError, TypeError, AttributeError): old_events = set() - await self.handle_room_pin(evt.room_id, evt.sender, new_events, old_events) + await self.handle_room_pin(evt.room_id, evt.sender, new_events, old_events, + evt.event_id) elif evt.type == EventType.ROOM_TOMBSTONE: - await self.handle_room_upgrade(evt.room_id, evt.sender, evt.content.replacement_room) + await self.handle_room_upgrade(evt.room_id, evt.sender, evt.content.replacement_room, + evt.event_id) elif evt.type == EventType.ROOM_ENCRYPTION: portal = po.Portal.get_by_mxid(evt.room_id) if portal: diff --git a/mautrix_telegram/portal/base.py b/mautrix_telegram/portal/base.py index 722d77ef..ecad8cc8 100644 --- a/mautrix_telegram/portal/base.py +++ b/mautrix_telegram/portal/base.py @@ -30,7 +30,8 @@ from telethon.tl.types import (Channel, ChannelFull, Chat, ChatFull, ChatInviteE from mautrix.errors import MatrixRequestError, IntentError from mautrix.appservice import AppService, IntentAPI -from mautrix.types import RoomID, RoomAlias, UserID, EventType, PowerLevelStateEventContent +from mautrix.types import (RoomID, RoomAlias, UserID, EventID, EventType, + PowerLevelStateEventContent) from mautrix.util.simple_template import SimpleTemplate from mautrix.util.logging import TraceLogger @@ -501,7 +502,8 @@ class BasePortal(ABC): @abstractmethod def handle_matrix_power_levels(self, sender: 'u.User', new_levels: Dict[UserID, int], - old_levels: Dict[UserID, int]) -> Awaitable[None]: + old_levels: Dict[UserID, int], event_id: Optional[EventID] + ) -> Awaitable[None]: pass @abstractmethod diff --git a/mautrix_telegram/portal/matrix.py b/mautrix_telegram/portal/matrix.py index 6ba7ab61..047ff086 100644 --- a/mautrix_telegram/portal/matrix.py +++ b/mautrix_telegram/portal/matrix.py @@ -228,6 +228,13 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): message, entities = None, None return message, entities + async def _send_delivery_receipt(self, event_id: EventID) -> None: + if event_id and config["bridge.delivery_receipts"]: + try: + await self.az.intent.mark_read(self.mxid, event_id) + except Exception: + self.log.exception("Failed to send delivery receipt for %s", event_id) + async def _handle_matrix_text(self, sender_id: TelegramID, event_id: EventID, space: TelegramID, client: 'MautrixTelegramClient', content: TextMessageEventContent, reply_to: TelegramID) -> None: @@ -245,6 +252,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): parse_mode=self._matrix_event_to_entities, link_preview=lp) self._add_telegram_message_to_db(event_id, space, 0, response) + await self._send_delivery_receipt(event_id) async def _handle_matrix_file(self, sender_id: TelegramID, event_id: EventID, space: TelegramID, client: 'MautrixTelegramClient', @@ -307,6 +315,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): response = await client.send_media(self.peer, media, reply_to=reply_to, caption=caption, entities=entities) self._add_telegram_message_to_db(event_id, space, 0, response) + await self._send_delivery_receipt(event_id) async def _matrix_document_edit(self, client: 'MautrixTelegramClient', content: MessageEventContent, space: TelegramID, @@ -317,6 +326,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): response = await client.edit_message(self.peer, orig_msg.tgid, caption, file=media) self._add_telegram_message_to_db(event_id, space, -1, response) + await self._send_delivery_receipt(event_id) return True return False @@ -339,6 +349,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): response = await client.send_media(self.peer, media, reply_to=reply_to, caption=caption, entities=entities) self._add_telegram_message_to_db(event_id, space, 0, response) + await self._send_delivery_receipt(event_id) def _add_telegram_message_to_db(self, event_id: EventID, space: TelegramID, edit_index: int, response: TypeMessage) -> None: @@ -405,8 +416,8 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): else: self.log.trace("Unhandled Matrix event: %s", content) - async def handle_matrix_pin(self, sender: 'u.User', - pinned_message: Optional[EventID]) -> None: + async def handle_matrix_pin(self, sender: 'u.User', pinned_message: Optional[EventID], + pin_event_id: EventID) -> None: if self.peer_type != "chat" and self.peer_type != "channel": return try: @@ -419,10 +430,12 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): self.log.warning(f"Could not find pinned {pinned_message} in {self.mxid}") return await sender.client(UpdatePinnedMessageRequest(peer=self.peer, id=message.tgid)) + await self._send_delivery_receipt(pin_event_id) except ChatNotModifiedError: pass - async def handle_matrix_deletion(self, deleter: 'u.User', event_id: EventID) -> None: + async def handle_matrix_deletion(self, deleter: 'u.User', event_id: EventID, + redaction_event_id: EventID) -> None: real_deleter = deleter if not await deleter.needs_relaybot(self) else self.bot space = self.tgid if self.peer_type == "channel" else real_deleter.tgid message = DBMessage.get_by_mxid(event_id, self.mxid, space) @@ -430,6 +443,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): return if message.edit_index == 0: await real_deleter.client.delete_messages(self.peer, [message.tgid]) + await self._send_delivery_receipt(redaction_event_id) else: self.log.debug(f"Ignoring deletion of edit event {message.mxid} in {message.mx_room}") @@ -444,7 +458,8 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): pin_messages=moderator, add_admins=admin) async def handle_matrix_power_levels(self, sender: 'u.User', new_users: Dict[UserID, int], - old_users: Dict[UserID, int]) -> None: + old_users: Dict[UserID, int], event_id: Optional[EventID] + ) -> None: # TODO handle all power level changes and bridge exact admin rights to supergroups/channels for user, level in new_users.items(): if not user or user == self.main_intent.mxid or user == sender.mxid: @@ -460,15 +475,16 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): if user not in old_users or level != old_users[user]: await self._update_telegram_power_level(sender, user_id, level) - async def handle_matrix_about(self, sender: 'u.User', about: str) -> None: + async def handle_matrix_about(self, sender: 'u.User', about: str, event_id: EventID) -> None: if self.peer_type not in ("chat", "channel"): return peer = await self.get_input_entity(sender) await sender.client(EditChatAboutRequest(peer=peer, about=about)) self.about = about self.save() + await self._send_delivery_receipt(event_id) - async def handle_matrix_title(self, sender: 'u.User', title: str) -> None: + async def handle_matrix_title(self, sender: 'u.User', title: str, event_id: EventID) -> None: if self.peer_type not in ("chat", "channel"): return @@ -480,8 +496,10 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): self.dedup.register_outgoing_actions(response) self.title = title self.save() + await self._send_delivery_receipt(event_id) - async def handle_matrix_avatar(self, sender: 'u.User', url: ContentURI) -> None: + async def handle_matrix_avatar(self, sender: 'u.User', url: ContentURI, event_id: EventID + ) -> None: if self.peer_type not in ("chat", "channel"): # Invalid peer type return @@ -507,8 +525,10 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): self.photo_id = f"{size.location.volume_id}-{size.location.local_id}" self.save() break + await self._send_delivery_receipt(event_id) - async def handle_matrix_upgrade(self, sender: UserID, new_room: RoomID) -> None: + async def handle_matrix_upgrade(self, sender: UserID, new_room: RoomID, event_id: EventID + ) -> None: _, server = self.main_intent.parse_user_id(sender) old_room = self.mxid self.migrate_and_save_matrix(new_room) @@ -535,6 +555,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): return await self.update_matrix_room(user, entity, direct=self.peer_type == "user") self.log.info(f"{sender} upgraded room from {old_room} to {self.mxid}") + await self._send_delivery_receipt(event_id) def migrate_and_save_matrix(self, new_id: RoomID) -> None: try: diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index 3174b02d..e93d40ca 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.py @@ -155,7 +155,7 @@ class PortalMetadata(BasePortal, ABC): if levels.get_user_level(self.main_intent.mxid) == 100: levels = self._get_base_power_levels(levels, entity) await self.main_intent.set_power_levels(self.mxid, levels) - await self.handle_matrix_power_levels(source, levels.users, {}) + await self.handle_matrix_power_levels(source, levels.users, {}, None) async def invite_telegram(self, source: 'u.User', puppet: Union[p.Puppet, 'AbstractUser']) -> None: