Add Matrix->Telegram kicking and fix and improve things. Fixes #36

This commit is contained in:
Tulir Asokan
2018-02-01 23:22:08 +02:00
parent 5311facf97
commit e38cf82c40
8 changed files with 112 additions and 61 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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:

View File

@@ -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"<a href=\"https://matrix.to/#/{msg.mx_room}/{msg.mxid}\">Quote<br></a>"
if html:
html = quote + html
else:
html = quote + escape(text)
if msg:
quote = f"<a href=\"https://matrix.to/#/{msg.mx_room}/{msg.mxid}\">Quote<br></a>"
if html:
html = quote + html
else:
html = quote + escape(text)
if html:
html = html.replace("\n", "<br/>")

View File

@@ -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":

View File

@@ -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")

View File

@@ -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():

View File

@@ -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:]