From e38cf82c4044409d5283ab97075cd8ecd040695c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Feb 2018 23:22:08 +0200 Subject: [PATCH] Add Matrix->Telegram kicking and fix and improve things. Fixes #36 --- README.md | 2 +- mautrix_appservice/intent_api.py | 8 ++++ mautrix_telegram/commands.py | 6 +-- mautrix_telegram/formatter.py | 21 +++++---- mautrix_telegram/matrix.py | 39 +++++++++-------- mautrix_telegram/portal.py | 75 +++++++++++++++++++++++--------- mautrix_telegram/puppet.py | 16 ++++++- mautrix_telegram/user.py | 6 +-- 8 files changed, 112 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 58d00b17..c8468a7f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ The bridge does not do this automatically. * [ ] Membership actions * [x] Inviting puppets * [ ] Inviting Matrix users who have logged in to Telegram - * [ ] Kicking + * [x] Kicking * [ ] Joining (once room aliases have been implemented) * [x] Leaving * [ ] Room metadata changes diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py index c692a36f..4c2807e1 100644 --- a/mautrix_appservice/intent_api.py +++ b/mautrix_appservice/intent_api.py @@ -211,6 +211,14 @@ class IntentAPI: content["info"] = info return self.send_state_event(room_id, "m.room.avatar", content) + def add_room_alias(self, room_id, alias): + self._ensure_registered() + self.client.set_room_alias(room_id, alias) + + def remove_room_alias(self, alias): + self._ensure_registered() + self.client.remove_room_alias(alias) + def set_room_name(self, room_id, name): self._ensure_joined(room_id) self._ensure_has_power_level_for(room_id, "m.room.name") diff --git a/mautrix_telegram/commands.py b/mautrix_telegram/commands.py index 1ffd63bd..07eeabca 100644 --- a/mautrix_telegram/commands.py +++ b/mautrix_telegram/commands.py @@ -351,14 +351,14 @@ class CommandHandler: + f"Use power level 95 instead of 100 for admins.") supergroup = type == "supergroup" - types = { + type = { "supergroup": "channel", "channel": "channel", "chat": "chat", "group": "chat", - } + }[type] - portal = po.Portal(tgid=None, mxid=self._room_id, title=title, peer_type=types[type]) + portal = po.Portal(tgid=None, mxid=self._room_id, title=title, peer_type=type) try: portal.create_telegram_chat(sender, supergroup=supergroup) except ValueError as e: diff --git a/mautrix_telegram/formatter.py b/mautrix_telegram/formatter.py index 0c3366bc..ca6ba027 100644 --- a/mautrix_telegram/formatter.py +++ b/mautrix_telegram/formatter.py @@ -80,13 +80,11 @@ class MatrixParser(HTMLParser): reply = self.reply_regex.search(url) if mention: mxid = mention.group(1) - puppet_match = p.Puppet.mxid_regex.search(mxid) - if puppet_match: - user = p.Puppet.get(puppet_match.group(1), create=False) - else: - user = u.User.get_by_mxid(mxid, create=False) + user = p.Puppet.get_by_mxid(mxid, create=False) if not user: - return + user = u.User.get_by_mxid(mxid, create=False) + if not user: + return if user.username: EntityType = MessageEntityMention url = f"@{user.username}" @@ -218,11 +216,12 @@ def telegram_event_to_matrix(evt, source): if evt.reply_to_msg_id: msg = DBMessage.query.get((evt.reply_to_msg_id, source.tgid)) - quote = f"Quote
" - if html: - html = quote + html - else: - html = quote + escape(text) + if msg: + quote = f"Quote
" + if html: + html = quote + html + else: + html = quote + escape(text) if html: html = html.replace("\n", "
") diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 34873fe2..60289c99 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -32,16 +32,6 @@ class MatrixHandler: self.az.intent.set_display_name( self.config.get("appservice.bot_displayname", "Telegram bridge bot")) - def is_puppet(self, mxid): - match = Puppet.mxid_regex.match(mxid) - return True if match else False - - def get_puppet(self, mxid): - match = Puppet.mxid_regex.match(mxid) - if not match: - return None - return Puppet.get(int(match.group(1))) - def handle_puppet_invite(self, room, puppet, inviter): self.log.debug(f"{inviter} invited puppet for {puppet.tgid} to {room}") if not inviter.logged_in: @@ -98,7 +88,7 @@ class MatrixHandler: elif user == self.az.bot_mxid: self.az.intent.join_room(room) return - puppet = self.get_puppet(user) + puppet = Puppet.get_by_mxid(user) if puppet: self.handle_puppet_invite(room, puppet, inviter) return @@ -117,6 +107,7 @@ class MatrixHandler: "You are not whitelisted on this Telegram bridge.") return elif not user.logged_in: + # TODO[waiting-for-bots] once we have bot support, this won't be needed. portal.main_intent.kick(room, user.mxid, "You are not logged into this Telegram bridge.") return @@ -124,13 +115,22 @@ class MatrixHandler: self.log.debug(f"{user} joined {room}") # TODO join Telegram chat if applicable - def handle_part(self, room, user): + def handle_part(self, room, user, sender): self.log.debug(f"{user} left {room}") - user = User.get_by_mxid(user, create=False) + + sender = User.get_by_mxid(sender, create=False) + portal = Portal.get_by_mxid(room) - if user and portal and user.logged_in: - portal.leave_matrix(user) - # TODO check if the event was a puppet being kicked and handle accordingly. + if not portal: + return + + puppet = Puppet.get_by_mxid(user) + if sender and puppet: + portal.leave_matrix(puppet, sender) + + user = User.get_by_mxid(user, create=False) + if user and user.logged_in: + portal.leave_matrix(user, sender) def is_command(self, message): text = message.get("body", "") @@ -185,7 +185,8 @@ class MatrixHandler: portal.handle_matrix_power_levels(sender, new["users"], old["users"]) def filter_matrix_event(self, event): - return event["sender"] == self.az.bot_mxid or self.is_puppet(event["sender"]) + return (event["sender"] == self.az.bot_mxid + or Puppet.get_id_from_mxid(event["sender"]) is not None) def handle_event(self, evt): if self.filter_matrix_event(evt): @@ -194,11 +195,11 @@ class MatrixHandler: type = evt["type"] content = evt.get("content", {}) if type == "m.room.member": - membership = content.get("membership", {}) + membership = content.get("membership", "") if membership == "invite": self.handle_invite(evt["room_id"], evt["state_key"], evt["sender"]) elif membership == "leave": - self.handle_part(evt["room_id"], evt["state_key"]) + self.handle_part(evt["room_id"], evt["state_key"], evt["sender"]) elif membership == "join": self.handle_join(evt["room_id"], evt["state_key"]) elif type == "m.room.message": diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index 4b4221ea..fcbfbe9a 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -19,11 +19,12 @@ from telethon.tl.functions.messages import (GetFullChatRequest, EditChatAdminReq ExportChatInviteRequest, DeleteChatUserRequest) from telethon.tl.functions.channels import (GetParticipantsRequest, CreateChannelRequest, InviteToChannelRequest, ExportInviteRequest, - LeaveChannelRequest) + LeaveChannelRequest, EditBannedRequest) from telethon.errors.rpc_error_list import ChatAdminRequiredError, LocationInvalidError from telethon.tl.types import * from PIL import Image from io import BytesIO +from datetime import datetime import mimetypes import magic from .db import Portal as DBPortal, Message as DBMessage @@ -127,7 +128,17 @@ class Portal: puppet = p.Puppet.get(self.tgid) if direct else None intent = puppet.intent if direct else self.az.intent - # TODO set room alias if public channel. + # TODO fix aliases and enable + # if self.peer_type == "channel" and entity.username: + # public = True + # alias = self._get_room_alias(entity.username) + # else: + # public = False + # # TODO invite link alias? + # alias = None + + # room = intent.create_room(alias=alias, is_public=public, invitees=invites, name=title, + # is_direct=direct) room = intent.create_room(invitees=invites, name=title, is_direct=direct) if not room: raise Exception(f"Failed to create room for {self.tgid_log}") @@ -136,7 +147,7 @@ class Portal: self.by_mxid[self.mxid] = self self.save() - power_level_requirement = 0 if self.peer_type == "chat" else 50 + power_level_requirement = 0 if self.peer_type == "chat" and entity.admins_enabled else 50 levels = self.main_intent.get_power_levels(self.mxid) levels["ban"] = 100 levels["invite"] = 50 @@ -147,6 +158,11 @@ class Portal: self.main_intent.set_power_levels(self.mxid, levels) self.update_after_create(user, entity, direct, puppet) + def _get_room_alias(self, username=None): + username = username or self.username + return config.get("bridge.alias_template", "telegram_{groupname}").format( + groupname=username) + def sync_telegram_users(self, source, users=[]): for entity in users: puppet = p.Puppet.get(entity.id) @@ -187,8 +203,12 @@ class Portal: if self.peer_type == "channel": if self.username != entity.username: - # TODO update room alias + # TODO fix aliases and enable + # if self.username: + # self.main_intent.remove_room_alias(self._get_room_alias()) self.username = entity.username + # if self.username: + # self.main_intent.add_room_alias(self.mxid, self._get_room_alias()) changed = True changed = self.update_title(entity.title, self.main_intent) or changed @@ -244,7 +264,8 @@ class Portal: elif self.peer_type == "chat": link = user.client(ExportChatInviteRequest(chat_id=self.tgid)) elif self.peer_type == "channel": - link = user.client(ExportInviteRequest(channel=user.client.get_input_entity(self.peer))) + link = user.client( + ExportInviteRequest(channel=user.client.get_input_entity(self.peer))) else: raise ValueError(f"Invalid peer type '{self.peer_type}' for invite link.") @@ -268,16 +289,28 @@ class Portal: file_name = f"matrix_upload{mimetypes.guess_extension(mime)}" return file_name, None if file_name == body else body - def leave_matrix(self, user): + def leave_matrix(self, user, source): if self.peer_type == "user": self.main_intent.leave_room(self.mxid) self.delete() del self.by_tgid[self.tgid_full] del self.by_mxid[self.mxid] + elif source: + target = source.client.get_input_entity(PeerUser(user_id=user.tgid)) + if self.peer_type == "chat": + source.client(DeleteChatUserRequest(chat_id=self.tgid, user_id=target)) + else: + channel = source.client.get_input_entity(self.peer) + rights = ChannelBannedRights(datetime.fromtimestamp(0), False) + # FIXME This should work, but it doesn't :( + source.client(EditBannedRequest(channel=channel, + user_id=target, + banned_rights=rights)) elif self.peer_type == "chat": user.client(DeleteChatUserRequest(chat_id=self.tgid, user_id=InputUserSelf())) elif self.peer_type == "channel": - user.client(LeaveChannelRequest(channel=user.client.get_input_entity(self.peer))) + channel = user.client.get_input_entity(self.peer) + user.client(LeaveChannelRequest(channel=channel)) def handle_matrix_message(self, sender, message, event_id): type = message["msgtype"] @@ -326,10 +359,8 @@ class Portal: def handle_matrix_power_levels(self, sender, new_users, old_users): for user, level in new_users.items(): - puppet_match = p.Puppet.mxid_regex.search(user) - if puppet_match: - user_id = int(puppet_match.group(1)) - else: + user_id = p.Puppet.get_id_by_mxid(user) + if not user_id: mx_user = u.User.get_by_mxid(user, create=False) if not mx_user or not mx_user.tgid: continue @@ -350,9 +381,9 @@ class Portal: mx_user = u.User.get_by_mxid(user, create=False) if mx_user and mx_user.tgid: user_tgids.add(mx_user.tgid) - puppet_match = p.Puppet.mxid_regex.match(user) - if puppet_match: - user_tgids.add(int(puppet_match.group(1))) + puppet_id = p.Puppet.get_id_from_mxid(user) + if puppet_id: + user_tgids.add(puppet_id) return user_tgids def create_telegram_chat(self, source, supergroup=False): @@ -363,20 +394,23 @@ class Portal: invites = self._get_telegram_users_in_matrix_room() if len(invites) < 2: - # TODO when we get the option for a bot, this won't happen when the bot is activated. + # TODO[waiting-for-bots] This won't happen when the bot is enabled raise ValueError("Not enough Telegram users to create a chat") invites = [source.client.get_input_entity(id) for id in invites] if self.peer_type == "chat": updates = source.client(CreateChatRequest(title=self.title, users=invites)) + entity = updates.chats[0] elif self.peer_type == "channel": - updates = source.client(CreateChannelRequest(title=self.title, megagroup=supergroup)) - # TODO invite people + updates = source.client(CreateChannelRequest(title=self.title, about="", + megagroup=supergroup)) + entity = updates.chats[0] + source.client(InviteToChannelRequest(channel=source.client.get_input_entity(entity), + users=invites)) else: raise ValueError("Invalid peer type for Telegram chat creation") - entity = updates.chats[0] self.tgid = entity.id self.tg_receiver = self.tgid self.update_info(source, entity) @@ -386,9 +420,8 @@ class Portal: if self.peer_type == "chat": source.client(AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0)) elif self.peer_type == "channel": - source.client(InviteToChannelRequest(channel=self.peer, - users=[InputUser(user_id=puppet.tgid)], - fwd_limit=0)) + target = source.client.get_input_entity(PeerUser(user_id=puppet.tgid)) + source.client(InviteToChannelRequest(channel=self.peer, users=[target])) else: raise ValueError("Invalid peer type for Telegram user invite") diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py index f7f906c9..21622f57 100644 --- a/mautrix_telegram/puppet.py +++ b/mautrix_telegram/puppet.py @@ -70,8 +70,8 @@ class Puppet: "first name": info.first_name, "last name": info.last_name, } - preferences = config.get("bridge", {}).get("displayname_preference", - ["full name", "username", "phone"]) + preferences = config.get("bridge.displayname_preference", + ["full name", "username", "phone"]) for preference in preferences: name = data[preference] if name: @@ -136,6 +136,18 @@ class Puppet: return None + @classmethod + def get_by_mxid(cls, mxid, create=True): + tgid = cls.get_id_from_mxid(mxid) + return cls.get(tgid, create) if tgid else None + + @classmethod + def get_id_from_mxid(cls, mxid): + match = cls.mxid_regex.match(mxid) + if match: + return int(match.group(1)) + return None + @classmethod def find_by_username(cls, username): for _, puppet in cls.cache.items(): diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index edb67e2a..5388584c 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -41,11 +41,9 @@ class User: self.connected = False self.client = None - bridge_config = config.get("bridge", {}) + self.is_admin = self.mxid in config.get("bridge.admins", []) - self.is_admin = self.mxid in bridge_config.get("admins", []) - - whitelist = bridge_config.get("whitelist", None) or [self.mxid] + whitelist = config.get("bridge.whitelist", None) or [self.mxid] self.whitelisted = not whitelist or self.mxid in whitelist if not self.whitelisted: homeserver = self.mxid[self.mxid.index(":") + 1:]