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