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
+1 -1
View File
@@ -79,7 +79,7 @@ The bridge does not do this automatically.
* [ ] Membership actions * [ ] Membership actions
* [x] Inviting puppets * [x] Inviting puppets
* [ ] Inviting Matrix users who have logged in to Telegram * [ ] Inviting Matrix users who have logged in to Telegram
* [ ] Kicking * [x] Kicking
* [ ] Joining (once room aliases have been implemented) * [ ] Joining (once room aliases have been implemented)
* [x] Leaving * [x] Leaving
* [ ] Room metadata changes * [ ] Room metadata changes
+8
View File
@@ -211,6 +211,14 @@ class IntentAPI:
content["info"] = info content["info"] = info
return self.send_state_event(room_id, "m.room.avatar", content) 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): def set_room_name(self, room_id, name):
self._ensure_joined(room_id) self._ensure_joined(room_id)
self._ensure_has_power_level_for(room_id, "m.room.name") self._ensure_has_power_level_for(room_id, "m.room.name")
+3 -3
View File
@@ -351,14 +351,14 @@ class CommandHandler:
+ f"Use power level 95 instead of 100 for admins.") + f"Use power level 95 instead of 100 for admins.")
supergroup = type == "supergroup" supergroup = type == "supergroup"
types = { type = {
"supergroup": "channel", "supergroup": "channel",
"channel": "channel", "channel": "channel",
"chat": "chat", "chat": "chat",
"group": "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: try:
portal.create_telegram_chat(sender, supergroup=supergroup) portal.create_telegram_chat(sender, supergroup=supergroup)
except ValueError as e: except ValueError as e:
+10 -11
View File
@@ -80,13 +80,11 @@ class MatrixParser(HTMLParser):
reply = self.reply_regex.search(url) reply = self.reply_regex.search(url)
if mention: if mention:
mxid = mention.group(1) mxid = mention.group(1)
puppet_match = p.Puppet.mxid_regex.search(mxid) user = p.Puppet.get_by_mxid(mxid, create=False)
if puppet_match:
user = p.Puppet.get(puppet_match.group(1), create=False)
else:
user = u.User.get_by_mxid(mxid, create=False)
if not user: if not user:
return user = u.User.get_by_mxid(mxid, create=False)
if not user:
return
if user.username: if user.username:
EntityType = MessageEntityMention EntityType = MessageEntityMention
url = f"@{user.username}" url = f"@{user.username}"
@@ -218,11 +216,12 @@ def telegram_event_to_matrix(evt, source):
if evt.reply_to_msg_id: if evt.reply_to_msg_id:
msg = DBMessage.query.get((evt.reply_to_msg_id, source.tgid)) 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 msg:
if html: quote = f"<a href=\"https://matrix.to/#/{msg.mx_room}/{msg.mxid}\">Quote<br></a>"
html = quote + html if html:
else: html = quote + html
html = quote + escape(text) else:
html = quote + escape(text)
if html: if html:
html = html.replace("\n", "<br/>") html = html.replace("\n", "<br/>")
+20 -19
View File
@@ -32,16 +32,6 @@ class MatrixHandler:
self.az.intent.set_display_name( self.az.intent.set_display_name(
self.config.get("appservice.bot_displayname", "Telegram bridge bot")) 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): def handle_puppet_invite(self, room, puppet, inviter):
self.log.debug(f"{inviter} invited puppet for {puppet.tgid} to {room}") self.log.debug(f"{inviter} invited puppet for {puppet.tgid} to {room}")
if not inviter.logged_in: if not inviter.logged_in:
@@ -98,7 +88,7 @@ class MatrixHandler:
elif user == self.az.bot_mxid: elif user == self.az.bot_mxid:
self.az.intent.join_room(room) self.az.intent.join_room(room)
return return
puppet = self.get_puppet(user) puppet = Puppet.get_by_mxid(user)
if puppet: if puppet:
self.handle_puppet_invite(room, puppet, inviter) self.handle_puppet_invite(room, puppet, inviter)
return return
@@ -117,6 +107,7 @@ class MatrixHandler:
"You are not whitelisted on this Telegram bridge.") "You are not whitelisted on this Telegram bridge.")
return return
elif not user.logged_in: 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, portal.main_intent.kick(room, user.mxid,
"You are not logged into this Telegram bridge.") "You are not logged into this Telegram bridge.")
return return
@@ -124,13 +115,22 @@ class MatrixHandler:
self.log.debug(f"{user} joined {room}") self.log.debug(f"{user} joined {room}")
# TODO join Telegram chat if applicable # 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}") 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) portal = Portal.get_by_mxid(room)
if user and portal and user.logged_in: if not portal:
portal.leave_matrix(user) return
# TODO check if the event was a puppet being kicked and handle accordingly.
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): def is_command(self, message):
text = message.get("body", "") text = message.get("body", "")
@@ -185,7 +185,8 @@ class MatrixHandler:
portal.handle_matrix_power_levels(sender, new["users"], old["users"]) portal.handle_matrix_power_levels(sender, new["users"], old["users"])
def filter_matrix_event(self, event): 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): def handle_event(self, evt):
if self.filter_matrix_event(evt): if self.filter_matrix_event(evt):
@@ -194,11 +195,11 @@ class MatrixHandler:
type = evt["type"] type = evt["type"]
content = evt.get("content", {}) content = evt.get("content", {})
if type == "m.room.member": if type == "m.room.member":
membership = content.get("membership", {}) membership = content.get("membership", "")
if membership == "invite": if membership == "invite":
self.handle_invite(evt["room_id"], evt["state_key"], evt["sender"]) self.handle_invite(evt["room_id"], evt["state_key"], evt["sender"])
elif membership == "leave": 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": elif membership == "join":
self.handle_join(evt["room_id"], evt["state_key"]) self.handle_join(evt["room_id"], evt["state_key"])
elif type == "m.room.message": elif type == "m.room.message":
+54 -21
View File
@@ -19,11 +19,12 @@ from telethon.tl.functions.messages import (GetFullChatRequest, EditChatAdminReq
ExportChatInviteRequest, DeleteChatUserRequest) ExportChatInviteRequest, DeleteChatUserRequest)
from telethon.tl.functions.channels import (GetParticipantsRequest, CreateChannelRequest, from telethon.tl.functions.channels import (GetParticipantsRequest, CreateChannelRequest,
InviteToChannelRequest, ExportInviteRequest, InviteToChannelRequest, ExportInviteRequest,
LeaveChannelRequest) LeaveChannelRequest, EditBannedRequest)
from telethon.errors.rpc_error_list import ChatAdminRequiredError, LocationInvalidError from telethon.errors.rpc_error_list import ChatAdminRequiredError, LocationInvalidError
from telethon.tl.types import * from telethon.tl.types import *
from PIL import Image from PIL import Image
from io import BytesIO from io import BytesIO
from datetime import datetime
import mimetypes import mimetypes
import magic import magic
from .db import Portal as DBPortal, Message as DBMessage 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 puppet = p.Puppet.get(self.tgid) if direct else None
intent = puppet.intent if direct else self.az.intent 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) room = intent.create_room(invitees=invites, name=title, is_direct=direct)
if not room: if not room:
raise Exception(f"Failed to create room for {self.tgid_log}") raise Exception(f"Failed to create room for {self.tgid_log}")
@@ -136,7 +147,7 @@ class Portal:
self.by_mxid[self.mxid] = self self.by_mxid[self.mxid] = self
self.save() 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 = self.main_intent.get_power_levels(self.mxid)
levels["ban"] = 100 levels["ban"] = 100
levels["invite"] = 50 levels["invite"] = 50
@@ -147,6 +158,11 @@ class Portal:
self.main_intent.set_power_levels(self.mxid, levels) self.main_intent.set_power_levels(self.mxid, levels)
self.update_after_create(user, entity, direct, puppet) 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=[]): def sync_telegram_users(self, source, users=[]):
for entity in users: for entity in users:
puppet = p.Puppet.get(entity.id) puppet = p.Puppet.get(entity.id)
@@ -187,8 +203,12 @@ class Portal:
if self.peer_type == "channel": if self.peer_type == "channel":
if self.username != entity.username: 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 self.username = entity.username
# if self.username:
# self.main_intent.add_room_alias(self.mxid, self._get_room_alias())
changed = True changed = True
changed = self.update_title(entity.title, self.main_intent) or changed changed = self.update_title(entity.title, self.main_intent) or changed
@@ -244,7 +264,8 @@ class Portal:
elif self.peer_type == "chat": elif self.peer_type == "chat":
link = user.client(ExportChatInviteRequest(chat_id=self.tgid)) link = user.client(ExportChatInviteRequest(chat_id=self.tgid))
elif self.peer_type == "channel": 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: else:
raise ValueError(f"Invalid peer type '{self.peer_type}' for invite link.") 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)}" file_name = f"matrix_upload{mimetypes.guess_extension(mime)}"
return file_name, None if file_name == body else body 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": if self.peer_type == "user":
self.main_intent.leave_room(self.mxid) self.main_intent.leave_room(self.mxid)
self.delete() self.delete()
del self.by_tgid[self.tgid_full] del self.by_tgid[self.tgid_full]
del self.by_mxid[self.mxid] 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": elif self.peer_type == "chat":
user.client(DeleteChatUserRequest(chat_id=self.tgid, user_id=InputUserSelf())) user.client(DeleteChatUserRequest(chat_id=self.tgid, user_id=InputUserSelf()))
elif self.peer_type == "channel": 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): def handle_matrix_message(self, sender, message, event_id):
type = message["msgtype"] type = message["msgtype"]
@@ -326,10 +359,8 @@ class Portal:
def handle_matrix_power_levels(self, sender, new_users, old_users): def handle_matrix_power_levels(self, sender, new_users, old_users):
for user, level in new_users.items(): for user, level in new_users.items():
puppet_match = p.Puppet.mxid_regex.search(user) user_id = p.Puppet.get_id_by_mxid(user)
if puppet_match: if not user_id:
user_id = int(puppet_match.group(1))
else:
mx_user = u.User.get_by_mxid(user, create=False) mx_user = u.User.get_by_mxid(user, create=False)
if not mx_user or not mx_user.tgid: if not mx_user or not mx_user.tgid:
continue continue
@@ -350,9 +381,9 @@ class Portal:
mx_user = u.User.get_by_mxid(user, create=False) mx_user = u.User.get_by_mxid(user, create=False)
if mx_user and mx_user.tgid: if mx_user and mx_user.tgid:
user_tgids.add(mx_user.tgid) user_tgids.add(mx_user.tgid)
puppet_match = p.Puppet.mxid_regex.match(user) puppet_id = p.Puppet.get_id_from_mxid(user)
if puppet_match: if puppet_id:
user_tgids.add(int(puppet_match.group(1))) user_tgids.add(puppet_id)
return user_tgids return user_tgids
def create_telegram_chat(self, source, supergroup=False): def create_telegram_chat(self, source, supergroup=False):
@@ -363,20 +394,23 @@ class Portal:
invites = self._get_telegram_users_in_matrix_room() invites = self._get_telegram_users_in_matrix_room()
if len(invites) < 2: 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") raise ValueError("Not enough Telegram users to create a chat")
invites = [source.client.get_input_entity(id) for id in invites] invites = [source.client.get_input_entity(id) for id in invites]
if self.peer_type == "chat": if self.peer_type == "chat":
updates = source.client(CreateChatRequest(title=self.title, users=invites)) updates = source.client(CreateChatRequest(title=self.title, users=invites))
entity = updates.chats[0]
elif self.peer_type == "channel": elif self.peer_type == "channel":
updates = source.client(CreateChannelRequest(title=self.title, megagroup=supergroup)) updates = source.client(CreateChannelRequest(title=self.title, about="",
# TODO invite people megagroup=supergroup))
entity = updates.chats[0]
source.client(InviteToChannelRequest(channel=source.client.get_input_entity(entity),
users=invites))
else: else:
raise ValueError("Invalid peer type for Telegram chat creation") raise ValueError("Invalid peer type for Telegram chat creation")
entity = updates.chats[0]
self.tgid = entity.id self.tgid = entity.id
self.tg_receiver = self.tgid self.tg_receiver = self.tgid
self.update_info(source, entity) self.update_info(source, entity)
@@ -386,9 +420,8 @@ class Portal:
if self.peer_type == "chat": if self.peer_type == "chat":
source.client(AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0)) source.client(AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0))
elif self.peer_type == "channel": elif self.peer_type == "channel":
source.client(InviteToChannelRequest(channel=self.peer, target = source.client.get_input_entity(PeerUser(user_id=puppet.tgid))
users=[InputUser(user_id=puppet.tgid)], source.client(InviteToChannelRequest(channel=self.peer, users=[target]))
fwd_limit=0))
else: else:
raise ValueError("Invalid peer type for Telegram user invite") raise ValueError("Invalid peer type for Telegram user invite")
+14 -2
View File
@@ -70,8 +70,8 @@ class Puppet:
"first name": info.first_name, "first name": info.first_name,
"last name": info.last_name, "last name": info.last_name,
} }
preferences = config.get("bridge", {}).get("displayname_preference", preferences = config.get("bridge.displayname_preference",
["full name", "username", "phone"]) ["full name", "username", "phone"])
for preference in preferences: for preference in preferences:
name = data[preference] name = data[preference]
if name: if name:
@@ -136,6 +136,18 @@ class Puppet:
return None 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 @classmethod
def find_by_username(cls, username): def find_by_username(cls, username):
for _, puppet in cls.cache.items(): for _, puppet in cls.cache.items():
+2 -4
View File
@@ -41,11 +41,9 @@ class User:
self.connected = False self.connected = False
self.client = None 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 = config.get("bridge.whitelist", None) or [self.mxid]
whitelist = bridge_config.get("whitelist", None) or [self.mxid]
self.whitelisted = not whitelist or self.mxid in whitelist self.whitelisted = not whitelist or self.mxid in whitelist
if not self.whitelisted: if not self.whitelisted:
homeserver = self.mxid[self.mxid.index(":") + 1:] homeserver = self.mxid[self.mxid.index(":") + 1:]