diff --git a/ROADMAP.md b/ROADMAP.md
index 89b1763d..23b1f0e3 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -25,7 +25,7 @@
* [x] Matrix users who have logged into Telegram
* [x] Kicking
* [ ] Joining
- * [ ] Chat name as alias
+ * [x] Chat name as alias
* [ ] ‡ Chat invite link as alias
* [x] Leaving
* [x] Room metadata changes (name, topic, avatar)
@@ -74,7 +74,7 @@
* [x] At startup
* [x] When receiving invite or message
* [x] Private chat creation by inviting Matrix puppet of Telegram user to new room
- * [ ] Option to use bot to relay messages for unauthenticated Matrix users
+ * [x] Option to use bot to relay messages for unauthenticated Matrix users
* [ ] Option to use own Matrix account for messages sent from other Telegram clients
* [Commands](https://github.com/tulir/mautrix-telegram/wiki/Management-commands)
* [x] Logging in and out (`login` + code entering)
diff --git a/example-config.yaml b/example-config.yaml
index 1aa2a508..73d20d34 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -87,3 +87,5 @@ telegram:
# Get your own API keys at https://my.telegram.org/apps
api_id: 12345
api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz
+ # (Optional) Create your own bot at https://t.me/BotFather
+ #bot_token: 123456789:ABCD-QBPd3VrWRhg623xYh07WUWErYA9eMI
diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py
index 0fd002e0..a8434932 100644
--- a/mautrix_appservice/intent_api.py
+++ b/mautrix_appservice/intent_api.py
@@ -223,11 +223,14 @@ class IntentAPI:
# region Room actions
async def create_room(self, alias=None, is_public=False, name=None, topic=None,
- is_direct=False, invitees=None, initial_state=None):
+ is_direct=False, invitees=None, initial_state=None,
+ guests_can_join=False):
await self.ensure_registered()
content = {
- "visibility": "public" if is_public else "private",
+ "visibility": "private",
"is_direct": is_direct,
+ "preset": "private_chat" if is_public else "public_chat",
+ "guests_can_join": guests_can_join,
}
if alias:
content["room_alias_name"] = alias
@@ -326,6 +329,13 @@ class IntentAPI:
events.remove(event_id)
await self.set_pinned_messages(room_id, events)
+ async def set_join_rule(self, room_id, join_rule):
+ if join_rule not in ("public", "knock", "invite", "private"):
+ raise ValueError(f"Invalid join rule \"{join_rule}\"")
+ await self.send_state_event(room_id, "m.room.join_rules", {
+ "join_rule": join_rule,
+ })
+
async def get_event(self, room_id, event_id):
await self.ensure_joined(room_id)
return await self.client.request("GET", f"/rooms/{room_id}/event/{event_id}")
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index 9af163e8..1b6d9423 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -29,9 +29,12 @@ from .config import Config
from .matrix import MatrixHandler
from .db import init as init_db
+from .abstract_user import init as init_abstract_user
from .user import init as init_user, User
+from .bot import init as init_bot
from .portal import init as init_portal
from .puppet import init as init_puppet
+from .context import Context
log = logging.getLogger("mau")
time_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s@%(name)s] %(message)s")
@@ -76,14 +79,22 @@ loop = asyncio.get_event_loop()
appserv = AppService(config["homeserver.address"], config["homeserver.domain"],
config["appservice.as_token"], config["appservice.hs_token"],
config["appservice.bot_username"], log="mau.as", loop=loop)
-context = (appserv, db_session, config, loop)
+
+
+context = Context(appserv, db_session, config, loop, None, None)
with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start:
- MatrixHandler(context)
init_db(db_session)
+ init_abstract_user(context)
+ context.bot = init_bot(context)
+ context.mx = MatrixHandler(context)
init_portal(context)
init_puppet(context)
- startup_actions = init_user(context) + [start]
+ startup_actions = init_user(context) + [start, context.mx.init_as_bot()]
+
+ if context.bot:
+ startup_actions.append(context.bot.start())
+
try:
loop.run_until_complete(asyncio.gather(*startup_actions, loop=loop))
loop.run_forever()
diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py
new file mode 100644
index 00000000..8cfb033b
--- /dev/null
+++ b/mautrix_telegram/abstract_user.py
@@ -0,0 +1,101 @@
+# -*- coding: future_fstrings -*-
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2018 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import platform
+import os
+
+from .tgclient import MautrixTelegramClient
+from . import __version__
+from telethon.tl.types import *
+
+config = None
+
+
+class AbstractUser:
+ loop = None
+ log = None
+ db = None
+ az = None
+
+ def __init__(self):
+ self.connected = False
+ self.whitelisted = False
+ self.client = None
+ self.tgid = None
+
+ def _init_client(self):
+ self.log.debug(f"Initializing client for {self.name}")
+ device = f"{platform.system()} {platform.release()}"
+ sysversion = MautrixTelegramClient.__version__
+ self.client = MautrixTelegramClient(self.name,
+ config["telegram.api_id"],
+ config["telegram.api_hash"],
+ loop=self.loop,
+ app_version=__version__,
+ system_version=sysversion,
+ device_model=device)
+ self.client.add_update_handler(self._update_catch)
+
+ async def update(self, update):
+ raise NotImplementedError()
+
+ async def post_login(self):
+ raise NotImplementedError()
+
+ async def _update_catch(self, update):
+ try:
+ await self.update(update)
+ except Exception:
+ self.log.exception("Failed to handle Telegram update")
+
+ async def _get_dialogs(self, limit=None):
+ dialogs = await self.client.get_dialogs(limit=limit)
+ return [dialog.entity for dialog in dialogs if (
+ not isinstance(dialog.entity, (User, ChatForbidden, ChannelForbidden))
+ and not (isinstance(dialog.entity, Chat)
+ and (dialog.entity.deactivated or dialog.entity.left)))]
+
+ @property
+ def name(self):
+ raise NotImplementedError()
+
+ @property
+ def logged_in(self):
+ return self.client and self.client.is_user_authorized()
+
+ @property
+ def has_full_access(self):
+ return self.logged_in and self.whitelisted
+
+ async def start(self):
+ self.connected = await self.client.connect()
+
+ async def ensure_started(self, even_if_no_session=False):
+ if not self.whitelisted:
+ return self
+ elif not self.connected and (even_if_no_session or os.path.exists(f"{self.name}.session")):
+ return await self.start()
+ return self
+
+ def stop(self):
+ self.client.disconnect()
+ self.client = None
+ self.connected = False
+
+
+def init(context):
+ global config
+ AbstractUser.az, AbstractUser.db, config, AbstractUser.loop, _ = context
diff --git a/mautrix_telegram/bot.py b/mautrix_telegram/bot.py
new file mode 100644
index 00000000..0aaa2da8
--- /dev/null
+++ b/mautrix_telegram/bot.py
@@ -0,0 +1,81 @@
+# -*- coding: future_fstrings -*-
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2018 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+import logging
+
+from telethon.tl.types import *
+
+from .abstract_user import AbstractUser
+from .db import BotChat
+
+config = None
+
+
+class Bot(AbstractUser):
+ log = logging.getLogger("mau.bot")
+
+ def __init__(self, token):
+ super().__init__()
+ self.token = token
+ self.whitelisted = True
+ self._init_client()
+ self.chats = {chat.id for chat in BotChat.query.all()}
+
+ async def start(self):
+ await super().start()
+ if not self.logged_in:
+ await self.client.sign_in(bot_token=self.token)
+ await self.post_login()
+ return self
+
+ async def post_login(self):
+ info = await self.client.get_me()
+ self.tgid = info.id
+
+ async def update(self, update):
+ if not isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
+ return
+ elif not isinstance(update.message, MessageService):
+ return
+ action = update.message.action
+ to_id = update.message.to_id
+ to_id = to_id.chat_id if isinstance(to_id, PeerChat) else to_id.channel_id
+ if isinstance(action, MessageActionChatAddUser):
+ if self.tgid in action.users:
+ self.chats.add(to_id)
+ self.db.add(BotChat(id=to_id))
+ self.db.commit()
+ elif isinstance(action, MessageActionChatDeleteUser):
+ if action.user_id == self.tgid:
+ self.chats.remove(to_id)
+ BotChat.query.get(to_id).delete()
+ self.db.commit()
+
+ def is_in_chat(self, peer_id):
+ return peer_id in self.chats
+
+ @property
+ def name(self):
+ return "bot"
+
+
+def init(context):
+ global config
+ config = context.config
+ token = config["telegram.bot_token"]
+ if token:
+ return Bot(token)
+ return None
diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py
index 6c114785..e96d47b0 100644
--- a/mautrix_telegram/commands/auth.py
+++ b/mautrix_telegram/commands/auth.py
@@ -44,6 +44,7 @@ async def login(evt):
elif len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp login `")
phone_number = evt.args[0]
+ await evt.sender.ensure_started(even_if_no_session=True)
await evt.sender.client.sign_in(phone_number)
evt.sender.command_status = {
"next": enter_code,
@@ -58,6 +59,7 @@ async def enter_code(evt):
return await evt.reply("**Usage:** `$cmdprefix+sp enter-code `")
try:
+ await evt.sender.ensure_started(even_if_no_session=True)
user = await evt.sender.client.sign_in(code=evt.args[0])
asyncio.ensure_future(evt.sender.post_login(user), loop=evt.loop)
evt.sender.command_status = None
@@ -98,6 +100,7 @@ async def enter_password(evt):
return await evt.reply("**Usage:** `$cmdprefix+sp enter-password `")
try:
+ await evt.sender.ensure_started(even_if_no_session=True)
user = await evt.sender.client.sign_in(password=evt.args[0])
asyncio.ensure_future(evt.sender.post_login(user), loop=evt.loop)
evt.sender.command_status = None
diff --git a/mautrix_telegram/commands/clean_rooms.py b/mautrix_telegram/commands/clean_rooms.py
index 96a2154c..12dcd144 100644
--- a/mautrix_telegram/commands/clean_rooms.py
+++ b/mautrix_telegram/commands/clean_rooms.py
@@ -52,7 +52,7 @@ async def _find_rooms(intent):
return management_rooms, unidentified_rooms, portals, empty_portals
-@command_handler(needs_admin=True, name="clean-rooms")
+@command_handler(needs_admin=True, needs_auth=False, name="clean-rooms")
async def clean_rooms(evt):
if not evt.is_management:
return await evt.reply("`clean-rooms` is a particularly spammy command. Please don't "
diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py
index 0e06e3a4..e7cf956f 100644
--- a/mautrix_telegram/commands/handler.py
+++ b/mautrix_telegram/commands/handler.py
@@ -87,7 +87,7 @@ class CommandHandler:
log = logging.getLogger("mau.commands")
def __init__(self, context):
- self.az, self.db, self.config, self.loop = context
+ self.az, self.db, self.config, self.loop, _ = context
self.command_prefix = self.config["bridge.command_prefix"]
# region Utility functions for handling commands
diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py
new file mode 100644
index 00000000..d53f7dc9
--- /dev/null
+++ b/mautrix_telegram/context.py
@@ -0,0 +1,34 @@
+# -*- coding: future_fstrings -*-
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2018 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+class Context:
+ def __init__(self, az, db, config, loop, bot, mx):
+ self.az = az
+ self.db = db
+ self.config = config
+ self.loop = loop
+ self.bot = bot
+ self.mx = mx
+
+ def __iter__(self):
+ yield self.az
+ yield self.db
+ yield self.config
+ yield self.loop
+ yield self.bot
+ # yield self.mx
diff --git a/mautrix_telegram/db.py b/mautrix_telegram/db.py
index b8a4b372..21bc3792 100644
--- a/mautrix_telegram/db.py
+++ b/mautrix_telegram/db.py
@@ -95,9 +95,17 @@ class Puppet(Base):
photo_id = Column(String, nullable=True)
+# Fucking Telegram not telling bots what chats they are in 3:<
+class BotChat(Base):
+ query = None
+ __tablename__ = "bot_chat"
+ id = Column(Integer, primary_key=True)
+
+
def init(db_session):
Portal.query = db_session.query_property()
Message.query = db_session.query_property()
UserPortal.query = db_session.query_property()
User.query = db_session.query_property()
Puppet.query = db_session.query_property()
+ BotChat.query = db_session.query_property()
diff --git a/mautrix_telegram/formatter.py b/mautrix_telegram/formatter.py
index 9590b5ec..8a7ea319 100644
--- a/mautrix_telegram/formatter.py
+++ b/mautrix_telegram/formatter.py
@@ -189,8 +189,9 @@ def matrix_reply_to_telegram(content, tg_space, room_id=None):
reply = content["m.relates_to"]["m.in_reply_to"]
room_id = room_id or reply["room_id"]
event_id = reply["event_id"]
- message = DBMessage.query.filter(DBMessage.mxid == event_id and
- DBMessage.tg_space == tg_space and
+ print(event_id, tg_space, room_id)
+ message = DBMessage.query.filter(DBMessage.mxid == event_id,
+ DBMessage.tg_space == tg_space,
DBMessage.mx_room == room_id).one_or_none()
if message:
return message.tgid
diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py
index f5f36adf..74ca003e 100644
--- a/mautrix_telegram/matrix.py
+++ b/mautrix_telegram/matrix.py
@@ -28,13 +28,13 @@ class MatrixHandler:
log = logging.getLogger("mau.mx")
def __init__(self, context):
- self.az, self.db, self.config, _ = context
+ self.az, self.db, self.config, _, self.tgbot = context
self.commands = CommandHandler(context)
self.az.matrix_event_handler(self.handle_event)
async def init_as_bot(self):
- self.az.intent.set_display_name(
+ await self.az.intent.set_display_name(
self.config.get("appservice.bot_displayname", "Telegram bridge bot"))
async def handle_puppet_invite(self, room, puppet, inviter):
@@ -88,7 +88,7 @@ class MatrixHandler:
"Telegram chat is created for this room.")
async def handle_invite(self, room, user, inviter):
- inviter = User.get_by_mxid(inviter)
+ inviter = await User.get_by_mxid(inviter).ensure_started()
if not inviter.whitelisted:
return
elif user == self.az.bot_mxid:
@@ -101,6 +101,9 @@ class MatrixHandler:
return
user = User.get_by_mxid(user, create=False)
+ if not user:
+ return
+ await user.ensure_started()
portal = Portal.get_by_mxid(room)
if user and user.has_full_access and portal:
await portal.invite_telegram(inviter, user)
@@ -110,7 +113,7 @@ class MatrixHandler:
self.log.debug(f"{inviter} invited {user} to {room}")
async def handle_join(self, room, user):
- user = User.get_by_mxid(user)
+ user = await User.get_by_mxid(user).ensure_started()
portal = Portal.get_by_mxid(room)
if not portal:
@@ -120,19 +123,23 @@ class MatrixHandler:
await portal.main_intent.kick(room, user.mxid,
"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.
+ elif not user.logged_in and not portal.has_bot:
await portal.main_intent.kick(room, user.mxid,
- "You are not logged into this Telegram bridge.")
+ "This chat does not have a bot relaying "
+ "messages for unauthenticated users.")
return
self.log.debug(f"{user} joined {room}")
- # TODO join Telegram chat if applicable
+ if user.logged_in:
+ await portal.join_matrix(user)
async def handle_part(self, room, user, sender):
self.log.debug(f"{user} left {room}")
sender = User.get_by_mxid(sender, create=False)
+ if not sender:
+ return
+ await sender.ensure_started()
portal = Portal.get_by_mxid(room)
if not portal:
@@ -143,7 +150,10 @@ class MatrixHandler:
await portal.leave_matrix(puppet, sender)
user = User.get_by_mxid(user, create=False)
- if user and user.logged_in:
+ if not user:
+ return
+ await user.ensure_started()
+ if user.logged_in:
await portal.leave_matrix(user, sender)
def is_command(self, message):
@@ -158,10 +168,12 @@ class MatrixHandler:
self.log.debug(f"{sender} sent {message} to ${room}")
is_command, text = self.is_command(message)
- sender = User.get_by_mxid(sender)
+ sender = await User.get_by_mxid(sender).ensure_started()
+ if not sender.whitelisted:
+ return
portal = Portal.get_by_mxid(room)
- if sender.has_full_access and portal and not is_command:
+ if not is_command and portal and (sender.logged_in or portal.has_bot):
await portal.handle_matrix_message(sender, message, event_id)
return
@@ -187,19 +199,19 @@ class MatrixHandler:
async def handle_redaction(self, room, sender, event_id):
portal = Portal.get_by_mxid(room)
- sender = User.get_by_mxid(sender)
+ sender = await User.get_by_mxid(sender).ensure_started()
if sender.has_full_access and portal:
await portal.handle_matrix_deletion(sender, event_id)
async def handle_power_levels(self, room, sender, new, old):
portal = Portal.get_by_mxid(room)
- sender = User.get_by_mxid(sender)
+ sender = await User.get_by_mxid(sender).ensure_started()
if sender.has_full_access and portal:
await portal.handle_matrix_power_levels(sender, new["users"], old["users"])
async def handle_room_meta(self, type, room, sender, content):
portal = Portal.get_by_mxid(room)
- sender = User.get_by_mxid(sender)
+ sender = await User.get_by_mxid(sender).ensure_started()
if sender.has_full_access and portal:
handler, content_key = {
"m.room.name": (portal.handle_matrix_title, "name"),
diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py
index c89c8fb8..421f7400 100644
--- a/mautrix_telegram/portal.py
+++ b/mautrix_telegram/portal.py
@@ -44,6 +44,7 @@ class Portal:
log = logging.getLogger("mau.portal")
db = None
az = None
+ bot = None
by_mxid = {}
by_tgid = {}
@@ -88,6 +89,10 @@ class Portal:
elif self.peer_type == "channel":
return PeerChannel(channel_id=self.tgid)
+ @property
+ def has_bot(self):
+ return self.bot and self.bot.is_in_chat(self.tgid)
+
def _hash_event(self, event):
if self.peer_type == "channel":
# Message IDs are unique per-channel
@@ -196,8 +201,7 @@ class Portal:
self._main_intent = puppet.intent if direct else self.az.intent
if self.peer_type == "channel" and entity.username:
- # TODO make public once safe
- public = False
+ public = True
alias = self._get_room_alias(entity.username)
self.username = entity.username
else:
@@ -206,7 +210,7 @@ class Portal:
alias = None
if alias:
- # TODO properly handle existing room aliases
+ # TODO? properly handle existing room aliases
await self.main_intent.remove_room_alias(alias)
power_levels = self._get_base_power_levels({}, entity)
@@ -319,6 +323,9 @@ class Portal:
self.username = username or None
if self.username:
await self.main_intent.add_room_alias(self.mxid, self._get_room_alias())
+ await self.main_intent.set_join_rule(self.mxid, "public")
+ else:
+ await self.main_intent.set_join_rule(self.mxid, "invite")
return True
return False
@@ -396,7 +403,7 @@ class Portal:
for member in members:
if p.Puppet.get_id_from_mxid(member) or member == self.main_intent.mxid:
continue
- user = u.User.get_by_mxid(member)
+ user = await u.User.get_by_mxid(member).ensure_started()
if user.has_full_access:
authenticated.append(user)
return authenticated
@@ -455,22 +462,42 @@ class Portal:
channel = await self.get_input_entity(user)
await user.client(LeaveChannelRequest(channel=channel))
+ async def join_matrix(self, user):
+ if self.peer_type == "channel":
+ await user.client(JoinChannelRequest(channel=await self.get_input_entity(user)))
+ else:
+ # We'll just assume the user is already in the chat.
+ pass
+
async def handle_matrix_message(self, sender, message, event_id):
type = message["msgtype"]
- space = self.tgid if self.peer_type == "channel" else sender.tgid
+ if sender.logged_in:
+ client = sender.client
+ space = self.tgid if self.peer_type == "channel" else sender.tgid
+ else:
+ client = self.bot.client
+ space = self.tgid if self.peer_type == "channel" else self.bot.tgid
reply_to = formatter.matrix_reply_to_telegram(message, space, room_id=self.mxid)
- if type in {"m.text", "m.emote"}:
+
+ if type == "m.emote":
+ if "formatted_body" in message:
+ message["formatted_body"] = f"* {sender.displayname} {message['formatted_body']}"
+ message["body"] = f"* {sender.displayname} {message['body']}"
+ type = "m.text"
+ elif not sender.logged_in:
+ if "formatted_body" in message:
+ message["formatted_body"] = \
+ f"<{sender.displayname}> {message['formatted_body']}"
+ message["body"] = f"<{sender.displayname}> {message['body']}"
+
+ if type == "m.text":
if "format" in message and message["format"] == "org.matrix.custom.html":
- message, entities = formatter.matrix_to_telegram(message["formatted_body"], space)
- if type == "m.emote":
- message = "/me " + message
- response = await sender.client.send_message(self.peer, message, entities=entities,
- reply_to=reply_to)
+ message, entities = formatter.matrix_to_telegram(message["formatted_body"])
+ response = await client.send_message(self.peer, message, entities=entities,
+ reply_to=reply_to)
else:
- if type == "m.emote":
- message["body"] = "/me " + message["body"]
- response = await sender.client.send_message(self.peer, message["body"],
- reply_to=reply_to)
+ response = await client.send_message(self.peer, message["body"],
+ reply_to=reply_to)
elif type in {"m.image", "m.file", "m.audio", "m.video"}:
file = await self.main_intent.download_file(message["url"])
@@ -483,8 +510,8 @@ class Portal:
if "w" in info and "h" in info:
attributes.append(DocumentAttributeImageSize(w=info["w"], h=info["h"]))
- response = await sender.client.send_file(self.peer, file, mime, caption, attributes,
- file_name, reply_to=reply_to)
+ response = await client.send_file(self.peer, file, mime, caption, attributes,
+ file_name, reply_to=reply_to)
else:
self.log.debug("Unhandled Matrix event: %s", message)
return
@@ -498,8 +525,8 @@ class Portal:
async def handle_matrix_deletion(self, deleter, event_id):
space = self.tgid if self.peer_type == "channel" else deleter.tgid
- message = DBMessage.query.filter(DBMessage.mxid == event_id and
- DBMessage.tg_space == space and
+ message = DBMessage.query.filter(DBMessage.mxid == event_id,
+ DBMessage.tg_space == space,
DBMessage.mx_room == self.mxid).one_or_none()
if not message:
return
@@ -627,7 +654,6 @@ class Portal:
invites = await self._get_telegram_users_in_matrix_room()
if len(invites) < 2:
- # TODO[waiting-for-bots] This won't happen when the bot is enabled
raise ValueError("Not enough Telegram users to create a chat")
if self.peer_type == "chat":
@@ -1065,4 +1091,4 @@ class Portal:
def init(context):
global config
- Portal.az, Portal.db, config, _ = context
+ Portal.az, Portal.db, config, _, Portal.bot = context
diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py
index 596bcd2e..2ee48c79 100644
--- a/mautrix_telegram/puppet.py
+++ b/mautrix_telegram/puppet.py
@@ -176,7 +176,7 @@ class Puppet:
def init(context):
global config
- Puppet.az, Puppet.db, config, _ = context
+ Puppet.az, Puppet.db, config, _, _ = context
localpart = config.get("bridge.username_template", "telegram_{userid}").format(userid="(.+)")
hs = config["homeserver"]["domain"]
Puppet.mxid_regex = re.compile(f"@{localpart}:{hs}")
diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py
index b1cc5aea..2a3637f4 100644
--- a/mautrix_telegram/user.py
+++ b/mautrix_telegram/user.py
@@ -16,31 +16,28 @@
# along with this program. If not, see .
import logging
import asyncio
-import platform
+import re
from telethon.tl.types import *
from telethon.tl.types.contacts import ContactsNotModified
-from telethon.tl.types import User as TLUser
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
from mautrix_appservice import MatrixRequestError
from .db import User as DBUser, Message as DBMessage, Contact as DBContact
-from .tgclient import MautrixTelegramClient
-from . import portal as po, puppet as pu, __version__
+from .abstract_user import AbstractUser
+from . import portal as po, puppet as pu
config = None
-class User:
- loop = None
+class User(AbstractUser):
log = logging.getLogger("mau.user")
- db = None
- az = None
by_mxid = {}
by_tgid = {}
def __init__(self, mxid, tgid=None, username=None, db_contacts=None, saved_contacts=0,
db_portals=None):
+ super().__init__()
self.mxid = mxid
self.tgid = tgid
self.username = username
@@ -51,9 +48,6 @@ class User:
self.db_portals = db_portals
self.command_status = None
- self.connected = False
- self.client = None
- self._init_client()
self.is_admin = self.mxid in config.get("bridge.admins", [])
@@ -67,13 +61,17 @@ class User:
if tgid:
self.by_tgid[tgid] = self
- @property
- def logged_in(self):
- return self.client.is_user_authorized()
+ self._init_client()
@property
- def has_full_access(self):
- return self.logged_in and self.whitelisted
+ def name(self):
+ return self.mxid
+
+ @property
+ def displayname(self):
+ # TODO show better username
+ match = re.compile("@(.+):(.+)").match(self.mxid)
+ return match.group(1)
@property
def db_contacts(self):
@@ -129,23 +127,13 @@ class User:
# endregion
# region Telegram connection management
- def _init_client(self):
- device = f"{platform.system()} {platform.release()}"
- sysversion = MautrixTelegramClient.__version__
- self.client = MautrixTelegramClient(self.mxid,
- config["telegram.api_id"],
- config["telegram.api_hash"],
- loop=self.loop,
- app_version=__version__,
- system_version=sysversion,
- device_model=device)
- self.client.add_update_handler(self.update_catch)
-
async def start(self, delete_unless_authenticated=False):
- self.connected = await self.client.connect()
+ await super().start()
if self.logged_in:
+ self.log.debug(f"Ensuring post_login() for {self.name}")
asyncio.ensure_future(self.post_login(), loop=self.loop)
elif delete_unless_authenticated:
+ self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting...")
# User not logged in -> forget user
self.client.disconnect()
self.client.session.delete()
@@ -160,11 +148,6 @@ class User:
except Exception:
self.log.exception("Failed to run post-login functions")
- def stop(self):
- self.client.disconnect()
- self.client = None
- self.connected = False
-
# endregion
# region Telegram actions that need custom methods
@@ -234,14 +217,8 @@ class User:
return await self._search_remote(query), True
async def sync_dialogs(self):
- dialogs = await self.client.get_dialogs(limit=30)
creators = []
- for dialog in dialogs:
- entity = dialog.entity
- invalid = (isinstance(entity, (TLUser, ChatForbidden, ChannelForbidden))
- or (isinstance(entity, Chat) and (entity.deactivated or entity.left)))
- if invalid:
- continue
+ for entity in await self._get_dialogs(limit=30):
portal = po.Portal.get_by_entity(entity)
self.portals[portal.tgid_full] = portal
creators.append(portal.create_matrix_room(self, entity, invites=[self.mxid]))
@@ -286,12 +263,6 @@ class User:
# endregion
# region Telegram update handling
- async def update_catch(self, update):
- try:
- await self.update(update)
- except Exception:
- self.log.exception("Failed to handle Telegram update")
-
async def update(self, update):
if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)):
@@ -340,7 +311,7 @@ class User:
await portal.set_telegram_admins_enabled(update.enabled)
elif isinstance(update, UpdateChatParticipantAdmin):
puppet = pu.Puppet.get(update.user_id)
- user = User.get_by_tgid(update.user_id)
+ user = await User.get_by_tgid(update.user_id).ensure_started()
await portal.set_telegram_admin(puppet, user)
async def update_typing(self, update):
@@ -425,14 +396,14 @@ class User:
user = DBUser.query.get(mxid)
if user:
user = cls.from_db(user)
- asyncio.ensure_future(user.start(), loop=cls.loop)
+ # asyncio.ensure_future(user.start(), loop=cls.loop)
return user
if create:
user = cls(mxid)
cls.db.add(user.to_db())
cls.db.commit()
- asyncio.ensure_future(user.start(), loop=cls.loop)
+ # asyncio.ensure_future(user.start(), loop=cls.loop)
return user
return None
@@ -447,7 +418,7 @@ class User:
user = DBUser.query.filter(DBUser.tgid == tgid).one_or_none()
if user:
user = cls.from_db(user)
- asyncio.ensure_future(user.start(), loop=cls.loop)
+ # asyncio.ensure_future(user.start(), loop=cls.loop)
return user
return None
@@ -468,7 +439,7 @@ class User:
def init(context):
global config
- User.az, User.db, config, User.loop = context
+ config = context.config
users = [User.from_db(user) for user in DBUser.query.all()]
return [user.start(delete_unless_authenticated=True) for user in users]