From 4766d143594b322abe9f13a135eefcde8404a129 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 4 Mar 2022 16:12:02 +0200 Subject: [PATCH] Move DM creation code to mautrix-python --- mautrix_telegram/commands/portal/bridge.py | 2 +- mautrix_telegram/matrix.py | 108 +++--------------- mautrix_telegram/portal.py | 94 ++++++++------- mautrix_telegram/user.py | 7 ++ mautrix_telegram/web/provisioning/__init__.py | 2 +- requirements.txt | 2 +- 6 files changed, 83 insertions(+), 132 deletions(-) diff --git a/mautrix_telegram/commands/portal/bridge.py b/mautrix_telegram/commands/portal/bridge.py index 0f7d9569..89f6f625 100644 --- a/mautrix_telegram/commands/portal/bridge.py +++ b/mautrix_telegram/commands/portal/bridge.py @@ -246,7 +246,7 @@ async def _locked_confirm_bridge( await portal.save() await portal.update_bridge_info() - asyncio.create_task(portal.update_matrix_room(user, entity, direct=False, levels=levels)) + asyncio.create_task(portal.update_matrix_room(user, entity, levels=levels)) await warn_missing_power(levels, evt) diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 4924063b..8db3a31c 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -61,98 +61,22 @@ class MatrixHandler(BaseMatrixHandler): self._previously_typing = {} - async def handle_puppet_invite( - self, room_id: RoomID, puppet: pu.Puppet, inviter: u.User, event_id: EventID + async def handle_puppet_group_invite( + self, + room_id: RoomID, + puppet: pu.Puppet, + invited_by: u.User, + evt: StateEvent, + members: list[UserID], ) -> None: - intent = puppet.default_mxid_intent - self.log.debug(f"{inviter.mxid} invited puppet for {puppet.tgid} to {room_id}") - if puppet.is_channel: - self.log.debug(f"Rejecting invite for {puppet.tgid} to {room_id}: puppet is a channel") - await intent.leave_room(room_id, reason="Channels can't be invited to chats") - return - - if not await inviter.is_logged_in(): - self.log.debug(f"Rejecting invite for {puppet.tgid} to {room_id}: user not logged in") - await intent.leave_room( - room_id, - reason="Only users who are logged into the bridge can invite Telegram ghosts.", - ) - return - - portal = await po.Portal.get_by_mxid(room_id) - if portal: - if portal.peer_type == "user": - await intent.error_and_leave( - room_id, text="You can not invite additional users to private chats." - ) - return - await portal.invite_telegram(inviter, puppet) - await intent.join_room(room_id) - return - try: - members = await intent.get_room_members(room_id) - except MatrixError: - self.log.exception(f"Failed to get members after joining {room_id} as {intent.mxid}") - return if self.az.bot_mxid not in members: - if len(members) > 2: - await intent.error_and_leave( - room_id, - text=None, - html=( - f"Please invite " - f"the bridge bot " - f"first if you want to create a Telegram chat." - ), - ) - return - - await intent.join_room(room_id) - portal = await po.Portal.get_by_tgid( - puppet.tgid, tg_receiver=inviter.tgid, peer_type="user" + await puppet.default_mxid_intent.leave_room( + room_id, reason="This ghost does not join multi-user rooms without the bridge bot." ) - if portal.mxid: - try: - await portal.invite_to_matrix(inviter.mxid) - await intent.send_notice( - room_id, - text=f"You already have a private chat with me: {portal.mxid}", - html=( - "You already have a private chat with me: " - f"Link to room" - ), - ) - await intent.leave_room(room_id) - return - except MatrixError: - pass - portal.mxid = room_id - e2be_ok = await portal.check_dm_encryption() - await portal.save() - await inviter.register_portal(portal) - if e2be_ok is True: - evt_type, content = await self.e2ee.encrypt( - room_id, - EventType.ROOM_MESSAGE, - TextMessageEventContent( - msgtype=MessageType.NOTICE, - body=( - "Portal to private chat created and end-to-bridge encryption enabled." - ), - ), - ) - await intent.send_message_event(room_id, evt_type, content) - else: - message = "Portal to private chat created." - if e2be_ok is False: - message += "\n\nWarning: Failed to enable end-to-bridge encryption" - await intent.send_notice(room_id, message) - await portal.update_bridge_info() else: - await intent.join_room(room_id) - await intent.send_notice( + await puppet.default_mxid_intent.send_notice( room_id, - "This puppet will remain inactive until a Telegram chat is created for this room.", + "This ghost will remain inactive until a Telegram chat is created for this room.", ) async def handle_invite( @@ -163,9 +87,13 @@ class MatrixHandler(BaseMatrixHandler): return await user.ensure_started() portal = await po.Portal.get_by_mxid(room_id) - if user and await user.has_full_access(allow_bot=True): - if portal and portal.allow_bridging: - await portal.invite_telegram(inviter, user) + if ( + user + and portal + and await user.has_full_access(allow_bot=True) + and portal.allow_bridging + ): + await portal.handle_matrix_invite(inviter, user) async def handle_join(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None: user = await u.User.get_and_start_by_mxid(user_id) diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index 540e6833..5cb7a4fa 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -163,7 +163,7 @@ from telethon.utils import decode_waveform import magic from mautrix.appservice import DOUBLE_PUPPET_SOURCE_KEY, IntentAPI -from mautrix.bridge import BasePortal, NotificationDisabler, async_getter_lock +from mautrix.bridge import BasePortal, NotificationDisabler, RejectMatrixInvite, async_getter_lock from mautrix.errors import IntentError, MatrixRequestError, MForbidden from mautrix.types import ( ContentURI, @@ -448,6 +448,14 @@ class Portal(DBPortal, BasePortal): # endregion # region Matrix -> Telegram metadata + async def save(self) -> None: + if self.deleted: + await super().insert() + await self.postinit() + self.deleted = False + else: + await super().save() + async def get_telegram_users_in_matrix_room( self, source: u.User, pre_create: bool = False ) -> tuple[list[InputPeerUser], list[UserID]]: @@ -570,18 +578,25 @@ class Portal(DBPortal, BasePortal): await self.handle_matrix_power_levels(source, levels.users, {}, None) await self.update_bridge_info() - async def invite_telegram(self, source: u.User, puppet: p.Puppet | au.AbstractUser) -> None: + async def handle_matrix_invite( + self, invited_by: u.User, puppet: p.Puppet | au.AbstractUser + ) -> None: if puppet.is_channel: raise ValueError("Can't invite channels to chats") - if self.peer_type == "chat": - await source.client( - AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0) - ) - elif self.peer_type == "channel": - await source.client(InviteToChannelRequest(channel=self.peer, users=[puppet.tgid])) - # We don't care if there are invites for private chat portals with the relaybot. - elif not self.bot or self.tg_receiver != self.bot.tgid: - raise ValueError("Invalid peer type for Telegram user invite") + try: + if self.peer_type == "chat": + await invited_by.client( + AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0) + ) + elif self.peer_type == "channel": + await invited_by.client( + InviteToChannelRequest(channel=self.peer, users=[puppet.tgid]) + ) + # We don't care if there are invites for private chat portals with the relaybot. + elif not self.bot or self.tg_receiver != self.bot.tgid: + raise RejectMatrixInvite("You can't invite additional users to private chats.") + except RPCError as e: + raise RejectMatrixInvite(e.message) from e # endregion # region Telegram -> Matrix metadata @@ -613,15 +628,12 @@ class Portal(DBPortal, BasePortal): self, user: au.AbstractUser, entity: TypeChat | User, - direct: bool = None, puppet: p.Puppet = None, levels: PowerLevelStateEventContent = None, users: list[User] = None, ) -> None: - if direct is None: - direct = self.peer_type == "user" try: - await self._update_matrix_room(user, entity, direct, puppet, levels, users) + await self._update_matrix_room(user, entity, puppet, levels, users) except Exception: self.log.exception("Fatal error updating Matrix room") @@ -629,12 +641,11 @@ class Portal(DBPortal, BasePortal): self, user: au.AbstractUser, entity: TypeChat | User, - direct: bool, puppet: p.Puppet = None, levels: PowerLevelStateEventContent = None, users: list[User] = None, ) -> None: - if not direct: + if not self.is_direct: await self.update_info(user, entity) if not users: users = await self._get_users(user, entity) @@ -642,7 +653,7 @@ class Portal(DBPortal, BasePortal): await self.update_power_levels(users, levels) else: if not puppet: - puppet = await p.Puppet.get_by_tgid(self.tgid) + puppet = await self.get_dm_puppet() await puppet.update_info(user, entity) await puppet.intent_for(self).join_room(self.mxid) await self.update_info_from_puppet(puppet, user, entity.photo) @@ -661,12 +672,14 @@ class Portal(DBPortal, BasePortal): async def update_info_from_puppet( self, - puppet: p.Puppet, + puppet: p.Puppet | None = None, source: au.AbstractUser | None = None, photo: UserProfilePhoto | None = None, ) -> None: if not self.encrypted and not self.private_chat_portal_meta: return + if puppet is None: + puppet = await self.get_dm_puppet() # The bridge bot needs to join for e2ee, but that messes up the default name # generation. If/when canonical DMs happen, this might not be necessary anymore. changed = await self._update_avatar_from_puppet(puppet, source, photo) @@ -690,7 +703,7 @@ class Portal(DBPortal, BasePortal): except Exception: self.log.exception(f"Failed to get entity through {user.tgid} for update") return self.mxid - update = self.update_matrix_room(user, entity, self.peer_type == "user") + update = self.update_matrix_room(user, entity) asyncio.create_task(update) await self.invite_to_matrix(invites or []) return self.mxid @@ -754,7 +767,6 @@ class Portal(DBPortal, BasePortal): elif not self.allow_bridging: return None - direct = self.peer_type == "user" invites = invites or [] if not entity: @@ -768,14 +780,14 @@ class Portal(DBPortal, BasePortal): except AttributeError: self.title = None - if direct and self.tgid == user.tgid: + if self.is_direct and self.tgid == user.tgid: self.title = "Telegram Saved Messages" self.about = "Your Telegram cloud storage chat" - puppet = await p.Puppet.get_by_tgid(self.tgid) if direct else None + puppet = await self.get_dm_puppet() if puppet: await puppet.update_info(user, entity) - self._main_intent = puppet.intent_for(self) if direct else self.az.intent + self._main_intent = puppet.intent_for(self) if self.is_direct else self.az.intent if self.peer_type == "channel": self.megagroup = entity.megagroup @@ -796,7 +808,7 @@ class Portal(DBPortal, BasePortal): power_levels = putil.get_base_power_levels(self, entity=entity) users = None - if not direct: + if not self.is_direct: users = await self._get_users(user, entity) if self.has_bot: extra_invites = self.config["bridge.relaybot.group_chat_invite"] @@ -836,9 +848,9 @@ class Portal(DBPortal, BasePortal): "content": {"algorithm": "m.megolm.v1.aes-sha2"}, } ) - if direct: + if self.is_direct: create_invites.append(self.az.bot_mxid) - if direct and (self.encrypted or self.private_chat_portal_meta): + if self.is_direct and (self.encrypted or self.private_chat_portal_meta): self.title = puppet.displayname self.avatar_url = puppet.avatar_url self.photo_id = puppet.photo_id @@ -857,7 +869,7 @@ class Portal(DBPortal, BasePortal): room_id = await self.main_intent.create_room( alias_localpart=alias, preset=preset, - is_direct=direct, + is_direct=self.is_direct, invitees=create_invites, name=self.title, topic=self.about, @@ -869,7 +881,7 @@ class Portal(DBPortal, BasePortal): self.name_set = bool(self.title) self.avatar_set = bool(self.avatar_url) - if self.encrypted and self.matrix.e2ee and direct: + if self.encrypted and self.matrix.e2ee and self.is_direct: try: await self.az.intent.ensure_joined(room_id) except Exception: @@ -885,7 +897,7 @@ class Portal(DBPortal, BasePortal): update_room = asyncio.create_task( self.update_matrix_room( - user, entity, direct, puppet, levels=power_levels, users=users + user, entity, self.is_direct, puppet, levels=power_levels, users=users ) ) @@ -2165,7 +2177,7 @@ class Portal(DBPortal, BasePortal): "Failed to fully migrate to upgraded Matrix room: no Telegram user found." ) return - await self.update_matrix_room(user, entity, direct=self.peer_type == "user") + await self.update_matrix_room(user, entity) self.log.info(f"{sender} upgraded room from {old_room} to {self.mxid}") await self._send_delivery_receipt(event_id, room_id=old_room) @@ -2178,13 +2190,6 @@ class Portal(DBPortal, BasePortal): self.by_mxid[self.mxid] = self await self.save() - async def enable_dm_encryption(self) -> bool: - ok = await super().enable_dm_encryption() - if ok: - puppet = await p.Puppet.get_by_tgid(self.tgid) - await self.update_info_from_puppet(puppet) - return ok - # endregion # region Telegram -> Matrix bridging @@ -3481,6 +3486,12 @@ class Portal(DBPortal, BasePortal): del self.by_mxid[self.mxid] except KeyError: pass + self.name_set = False + self.avatar_set = False + self.about = None + self.sponsored_event_id = None + self.sponsored_event_ts = None + self.sponsored_msg_random_id = None await super().delete() await DBMessage.delete_all(self.mxid) await DBReaction.delete_all(self.mxid) @@ -3489,8 +3500,13 @@ class Portal(DBPortal, BasePortal): # endregion # region Class instance lookup + async def get_dm_puppet(self) -> p.Puppet | None: + if not self.is_direct: + return None + return await p.Puppet.get_by_tgid(self.tgid) + async def postinit(self) -> None: - puppet = await p.Puppet.get_by_tgid(self.tgid) if self.is_direct else None + puppet = await self.get_dm_puppet() self._main_intent = puppet.intent_for(self) if self.is_direct else self.az.intent if self.tgid: diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 9a382793..a25a77bb 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -269,6 +269,13 @@ class User(DBUser, AbstractUser, BaseUser): return None return await pu.Puppet.get_by_tgid(self.tgid) + async def get_portal_with(self, puppet: pu.Puppet, create: bool = True) -> po.Portal | None: + if not self.tgid: + return None + return await po.Portal.get_by_tgid( + puppet.tgid, tg_receiver=self.tgid, peer_type="user" if create else None + ) + async def stop(self) -> None: if self._track_connection_task: self._track_connection_task.cancel() diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 3e40c4e3..9e7ab778 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -212,7 +212,7 @@ class ProvisioningAPI(AuthAPI): portal.photo_id = "" await portal.save() - asyncio.create_task(portal.update_matrix_room(user, entity, direct=False, levels=levels)) + asyncio.create_task(portal.update_matrix_room(user, entity, levels=levels)) return web.Response(status=202, body="{}") diff --git a/requirements.txt b/requirements.txt index dbe0f5c0..d10f4b1f 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.15.0rc4 +mautrix==0.15.0rc5 #telethon>=1.24,<1.25 # Fork to make session storage async and update to layer 138 tulir-telethon==1.25.0a5