diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py index 49bf5e41..0b6f78ee 100644 --- a/mautrix_appservice/intent_api.py +++ b/mautrix_appservice/intent_api.py @@ -446,8 +446,12 @@ class IntentAPI: return self.client.request("POST", f"/join/{quote(room)}") def leave_room(self, room_id): - self.state_store.left(room_id, self.mxid) - return self.client.request("POST", f"/rooms/{quote(room_id)}/leave") + try: + self.state_store.left(room_id, self.mxid) + return self.client.request("POST", f"/rooms/{quote(room_id)}/leave") + except MatrixRequestError as e: + if "not in room" not in e.message: + raise def get_room_memberships(self, room_id): return self.client.request("GET", f"/rooms/{quote(room_id)}/members") diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 798d9c2b..030298d4 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -103,7 +103,7 @@ class CommandHandler: evt.command = "" command = sender.command_status["next"] else: - command = command_handlers["unknown_command"] + command = command_handlers["unknown-command"] try: await command(evt) except FloodWaitError as e: diff --git a/mautrix_telegram/commands/telegram.py b/mautrix_telegram/commands/telegram.py index 6b157d63..ea5d6950 100644 --- a/mautrix_telegram/commands/telegram.py +++ b/mautrix_telegram/commands/telegram.py @@ -101,7 +101,7 @@ async def delete_portal(evt): that_this = "This" if room_id == evt.room_id else "That" return await evt.reply(f"{that_this} is not a portal room.") - async def post_confirm(_, confirm): + async def post_confirm(confirm): evt.sender.command_status = None if len(confirm.args) > 0 and confirm.args[0] == "confirm-delete": await portal.cleanup_and_delete() diff --git a/mautrix_telegram/formatter.py b/mautrix_telegram/formatter.py index 57ab93c7..45ce3cf3 100644 --- a/mautrix_telegram/formatter.py +++ b/mautrix_telegram/formatter.py @@ -280,6 +280,12 @@ async def telegram_event_to_matrix(evt, source, native_replies=False, message_li else: html = quote + escape(text) + if evt.post and evt.post_author: + if not html: + html = escape(text) + text += f"\n- {evt.post_author}" + html += f"
- {evt.post_author}" + if html: html = html.replace("\n", "
") diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index f44b59a4..798f831b 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -149,12 +149,14 @@ class Portal: else: raise ValueError("Invalid invite identifier given to invite_matrix()") - async def update_after_create(self, user, entity, direct, puppet=None): + async def update_after_create(self, user, entity, direct, puppet=None, + levels=None, users=None, participants=None): if not direct: await self.update_info(user, entity) - users, participants = await self.get_users(user, entity) + if not users or not participants: + users, participants = await self.get_users(user, entity) await self.sync_telegram_users(user, users) - await self.update_telegram_participants(participants) + await self.update_telegram_participants(participants, levels) else: if not puppet: puppet = p.Puppet.get(self.tgid) @@ -190,7 +192,7 @@ class Portal: self.title = None puppet = p.Puppet.get(self.tgid) if direct else None - intent = puppet.intent if direct else self.az.intent + self._main_intent = puppet.intent if direct else self.az.intent if self.peer_type == "channel" and entity.username: # TODO make public once safe @@ -204,27 +206,52 @@ class Portal: if alias: # TODO properly handle existing room aliases - await intent.remove_room_alias(alias) - room = await intent.create_room(alias=alias, is_public=public, invitees=invites or [], - name=self.title, is_direct=direct) + await self.main_intent.remove_room_alias(alias) + + power_levels = self._get_base_power_levels({}, entity) + users = participants = None + if not direct: + users, participants = await self.get_users(user, entity) + self._participants_to_power_levels(participants, power_levels) + initial_state = [{ + "type": "m.room.power_levels", + "content": power_levels, + }] + + room = await self.main_intent.create_room(alias=alias, is_public=public, is_direct=direct, + invitees=invites or [], name=self.title, + initial_state=initial_state) if not room: raise Exception(f"Failed to create room for {self.tgid_log}") self.mxid = room["room_id"] self.by_mxid[self.mxid] = self self.save() + self.az.state_store.set_power_levels(self.mxid, power_levels) user.register_portal(self) + await self.update_after_create(user, entity, direct, puppet, + levels=power_levels, users=users, participants=participants) - power_level_requirement = 0 if self.peer_type == "chat" and entity.admins_enabled else 50 - levels = await self.main_intent.get_power_levels(self.mxid) - levels["ban"] = 100 - levels["invite"] = 50 + def _get_base_power_levels(self, levels=None, entity=None): + levels = levels or {} + power_level_requirement = (0 if self.peer_type == "chat" and not entity.admins_enabled + else 50) + levels["ban"] = 99 + levels["invite"] = power_level_requirement if self.peer_type == "chat" else 75 + if "events" not in levels: + levels["events"] = {} levels["events"]["m.room.name"] = power_level_requirement levels["events"]["m.room.avatar"] = power_level_requirement - levels["events"]["m.room.topic"] = 50 if self.peer_type == "channel" else 100 + levels["events"]["m.room.topic"] = 50 if self.peer_type == "channel" else 99 levels["events"]["m.room.power_levels"] = 75 - await self.main_intent.set_power_levels(self.mxid, levels) - await self.update_after_create(user, entity, direct, puppet) + levels["events"]["m.room.history_visibility"] = 75 + levels["events_default"] = (50 if self.peer_type == "channel" and not entity.megagroup + else 0) + if "users" not in levels: + levels["users"] = { + self.main_intent.mxid: 100 + } + return levels def _get_room_alias(self, username=None): username = username or self.username @@ -481,6 +508,8 @@ class Portal: async def handle_matrix_power_levels(self, sender, new_users, old_users): # 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: + continue user_id = p.Puppet.get_id_from_mxid(user) if not user_id: mx_user = u.User.get_by_mxid(user, create=False) @@ -501,9 +530,7 @@ class Portal: add_admins=admin, manage_call=moderator) await sender.client( EditAdminRequest(channel=await self.get_input_entity(sender), - user_id=await sender.client.get_input_entity( - PeerUser(user_id)), - admin_rights=rights)) + user_id=user_id, admin_rights=rights)) async def handle_matrix_about(self, sender, about): if self.peer_type not in {"channel"}: @@ -566,7 +593,7 @@ class Portal: puppet_id = p.Puppet.get_id_from_mxid(user) if puppet_id: user_tgids.add(puppet_id) - return user_tgids + return list(user_tgids) async def upgrade_telegram_chat(self, source): if self.peer_type != "chat": @@ -603,8 +630,6 @@ class Portal: # TODO[waiting-for-bots] This won't happen when the bot is enabled raise ValueError("Not enough Telegram users to create a chat") - invites = [await source.client.get_input_entity(id) for id in invites] - if self.peer_type == "chat": updates = await source.client(CreateChatRequest(title=self.title, users=invites)) entity = updates.chats[0] @@ -625,6 +650,12 @@ class Portal: await self.update_info(source, entity) self.save() + levels = await self.main_intent.get_power_levels(self.mxid) + levels = self._get_base_power_levels(levels, entity) + already_saved = await self.handle_matrix_power_levels(source, levels["users"], {}) + if not already_saved: + await self.main_intent.set_power_levels(self.mxid, levels) + async def invite_telegram(self, source, puppet): if self.peer_type == "chat": await source.client( @@ -642,11 +673,11 @@ class Portal: if self.mxid: await user.intent.set_typing(self.mxid, is_typing=True) - async def handle_telegram_photo(self, source, sender, media, relates_to=None): + async def handle_telegram_photo(self, source, intent, media, relates_to=None): largest_size = self._get_largest_photo_size(media.photo) file = await source.client.download_file_bytes(largest_size.location) mime_type = magic.from_buffer(file, mime=True) - uploaded = await sender.intent.upload_file(file, mime_type) + uploaded = await intent.upload_file(file, mime_type) info = { "h": largest_size.h, "w": largest_size.w, @@ -656,8 +687,8 @@ class Portal: "mimetype": mime_type, } name = media.caption - await sender.intent.set_typing(self.mxid, is_typing=False) - return await sender.intent.send_image(self.mxid, uploaded["content_uri"], info=info, + await intent.set_typing(self.mxid, is_typing=False) + return await intent.send_image(self.mxid, uploaded["content_uri"], info=info, text=name, relates_to=relates_to) def convert_webp(self, file, to="png"): @@ -670,14 +701,14 @@ class Portal: self.log.exception(f"Failed to convert webp to {to}") return "image/webp", file - async def handle_telegram_document(self, source, sender, media, relates_to=None): + async def handle_telegram_document(self, source, intent, media, relates_to=None): file = await source.client.download_file_bytes(media.document) mime_type = magic.from_buffer(file, mime=True) dont_change_mime = False if mime_type == "image/webp": mime_type, file = self.convert_webp(file, to="png") dont_change_mime = True - uploaded = await sender.intent.upload_file(file, mime_type) + uploaded = await intent.upload_file(file, mime_type) name = media.caption for attr in media.document.attributes: if not name and isinstance(attr, DocumentAttributeFilename): @@ -699,11 +730,11 @@ class Portal: type = "m.audio" elif mime_type.startswith("image/"): type = "m.image" - await sender.intent.set_typing(self.mxid, is_typing=False) - return await sender.intent.send_file(self.mxid, uploaded["content_uri"], info=info, + await intent.set_typing(self.mxid, is_typing=False) + return await intent.send_file(self.mxid, uploaded["content_uri"], info=info, text=name, file_type=type, relates_to=relates_to) - def handle_telegram_location(self, source, sender, location, relates_to=None): + def handle_telegram_location(self, source, intent, location, relates_to=None): long = location.long lat = location.lat long_char = "E" if long > 0 else "W" @@ -720,7 +751,7 @@ class Portal: # so we'll add a plaintext link. body = f"Location: {body}\n{url}" - return sender.intent.send_message(self.mxid, { + return intent.send_message(self.mxid, { "msgtype": "m.location", "geo_uri": f"geo:{lat},{long}", "body": body, @@ -729,17 +760,17 @@ class Portal: "m.relates_to": relates_to or None, }) - async def handle_telegram_text(self, source, sender, evt): - self.log.debug(f"Sending {evt.message} to {self.mxid} by {sender.id}") + async def handle_telegram_text(self, source, intent, evt): + self.log.debug(f"Sending {evt.message} to {self.mxid} by {intent.mxid}") text, html, relates_to = await formatter.telegram_event_to_matrix( evt, source, config["bridge.native_replies"], config["bridge.link_in_reply"], self.main_intent) - await sender.intent.set_typing(self.mxid, is_typing=False) - return await sender.intent.send_text(self.mxid, text, html=html, relates_to=relates_to) + await intent.set_typing(self.mxid, is_typing=False) + return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to) - async def handle_telegram_edit(self, source, sender, evt): + async def handle_telegram_edit(self, source, intent, evt): if not self.mxid: return elif not config["bridge.edits_as_replies"]: @@ -751,8 +782,8 @@ class Portal: config["bridge.native_replies"], config["bridge.link_in_reply"], self.main_intent, reply_text="Edit") - await sender.intent.set_typing(self.mxid, is_typing=False) - response = await sender.intent.send_text(self.mxid, text, html=html, relates_to=relates_to) + await intent.set_typing(self.mxid, is_typing=False) + response = await intent.send_text(self.mxid, text, html=html, relates_to=relates_to) mxid = response["event_id"] tg_space = self.tgid if self.peer_type == "channel" else source.tgid @@ -778,17 +809,18 @@ class Portal: self.db.commit() return + intent = sender.intent if sender else self.main_intent if evt.message: - response = await self.handle_telegram_text(source, sender, evt) + response = await self.handle_telegram_text(source, intent, evt) elif evt.media: relates_to = formatter.telegram_reply_to_matrix(evt, source) if isinstance(evt.media, MessageMediaPhoto): - response = await self.handle_telegram_photo(source, sender, evt.media, relates_to) + response = await self.handle_telegram_photo(source, intent, evt.media, relates_to) elif isinstance(evt.media, MessageMediaDocument): - response = await self.handle_telegram_document(source, sender, evt.media, + response = await self.handle_telegram_document(source, intent, evt.media, relates_to) elif isinstance(evt.media, MessageMediaGeo): - response = await self.handle_telegram_location(source, sender, evt.media.geo, + response = await self.handle_telegram_location(source, intent, evt.media.geo, relates_to) else: self.log.debug("Unhandled Telegram media: %s", evt.media) @@ -855,10 +887,8 @@ class Portal: else: await self.main_intent.set_pinned_messages(self.mxid, []) - async def update_telegram_participants(self, participants): - levels = await self.main_intent.get_power_levels(self.mxid) + def _participants_to_power_levels(self, participants, levels): changed = False - admin_power_level = 75 if self.peer_type == "channel" else 50 if levels["events"]["m.room.power_levels"] != admin_power_level: changed = True @@ -892,7 +922,12 @@ class Portal: if not puppet_has_right_level: levels["users"][puppet.mxid] = new_level changed = True - if changed: + return changed + + async def update_telegram_participants(self, participants, levels=None): + if not levels: + levels = await self.main_intent.get_power_levels(self.mxid) + if self._participants_to_power_levels(participants, levels): await self.main_intent.set_power_levels(self.mxid, levels) async def set_telegram_admins_enabled(self, enabled): diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 1c19dd96..7ff16cb0 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -113,6 +113,15 @@ class User: self.to_db() self.db.commit() + def delete(self): + try: + del self.by_mxid[self.mxid] + del self.by_tgid[self.tgid] + except KeyError: + pass + self.db.delete(self.to_db()) + self.db.commit() + @classmethod def from_db(cls, db_user): return User(db_user.mxid, db_user.tgid, db_user.tg_username, db_user.contacts, @@ -137,6 +146,10 @@ class User: self.connected = await self.client.connect() if self.logged_in: asyncio.ensure_future(self.post_login(), loop=self.loop) + else: + # User not logged in -> forget user + self.client.disconnect() + self.client.session.delete() return self async def post_login(self, info=None): @@ -186,8 +199,7 @@ class User: ok = await self.client.log_out() if not ok: return False - self._init_client() - await self.start() + self.delete() return True def _search_local(self, query, max_results=5, min_similarity=45): @@ -373,7 +385,7 @@ class User: tg_receiver=self.tgid) else: portal = po.Portal.get_by_entity(update.to_id, receiver_id=self.tgid) - sender = pu.Puppet.get(update.from_id) + sender = pu.Puppet.get(update.from_id) if update.from_id else None else: self.log.warning( f"Unexpected message type in User#get_message_details: {type(update)}") @@ -392,11 +404,12 @@ class User: sender.id) return portal.handle_telegram_action(self, sender, update.action) + user = sender.tgid if sender else "admin" if isinstance(original_update, (UpdateEditMessage, UpdateEditChannelMessage)): - self.log.debug("Handling edit %s to %s by %d", update, portal.tgid_log, sender.tgid) + self.log.debug("Handling edit %s to %s by %s", update, portal.tgid_log, user) return portal.handle_telegram_edit(self, sender, update) - self.log.debug("Handling message %s to %s by %d", update, portal.tgid_log, sender.tgid) + self.log.debug("Handling message %s to %s by %s", update, portal.tgid_log, user) return portal.handle_telegram_message(self, sender, update) # endregion