diff --git a/mautrix_telegram/commands.py b/mautrix_telegram/commands.py
deleted file mode 100644
index e9555b41..00000000
--- a/mautrix_telegram/commands.py
+++ /dev/null
@@ -1,523 +0,0 @@
-# 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 markdown
-import logging
-import asyncio
-
-from mautrix_appservice import MatrixRequestError
-
-from telethon.errors import *
-from telethon.tl.types import *
-from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest
-from telethon.tl.functions.channels import JoinChannelRequest
-
-from . import puppet as pu, portal as po
-
-command_handlers = {}
-
-
-def command_handler(func):
- command_handlers[func.__name__] = func
- return func
-
-
-def format_duration(seconds):
- def pluralize(count, singular): return singular if count == 1 else singular + "s"
-
- def include(count, word): return f"{count} {pluralize(count, word)}" if count > 0 else ""
-
- minutes, seconds = divmod(seconds, 60)
- hours, minutes = divmod(minutes, 60)
- days, hours = divmod(hours, 24)
- parts = [a for a in [
- include(days, "day"),
- include(hours, "hour"),
- include(minutes, "minute"),
- include(seconds, "second")] if a]
- if len(parts) > 2:
- return "{} and {}".format(", ".join(parts[:-1]), parts[-1])
- return " and ".join(parts)
-
-
-class CommandEvent:
- def __init__(self, az, command_prefix, room, sender, args, is_management, is_portal):
- self.az = az
- self.command_prefix = command_prefix
- self.room_id = room
- self.sender = sender
- self.args = args
- self.is_management = is_management
- self.is_portal = is_portal
-
- def reply(self, message, allow_html=False, render_markdown=True):
- if not self.room_id:
- raise AttributeError("the reply function can only be used from within"
- "the `CommandHandler.run` context manager")
-
- message = message.replace("$cmdprefix+sp ",
- "" if self.is_management else f"{self.command_prefix} ")
- message = message.replace("$cmdprefix", self.command_prefix)
- html = None
- if render_markdown:
- html = markdown.markdown(message, safe_mode="escape" if allow_html else False)
- elif allow_html:
- html = message
- return self.az.intent.send_notice(self.room_id, message, html=html)
-
-
-class CommandHandler:
- log = logging.getLogger("mau.commands")
-
- def __init__(self, context):
- self.az, self.db, self.config, self.loop = context
- self.command_prefix = self.config["bridge.command_prefix"]
-
- # region Utility functions for handling commands
-
- async def handle(self, room, sender, command, args, is_management, is_portal):
- evt = CommandEvent(self.az, self.command_prefix, room, sender, args, is_management,
- is_portal)
- command = command.lower()
- try:
- command = command_handlers[command]
- except KeyError:
- if sender.command_status and "next" in sender.command_status:
- args.insert(0, command)
- command = sender.command_status["next"]
- else:
- command = command_handlers["unknown_command"]
- try:
- await command(self, evt)
- except FloodWaitError as e:
- return evt.reply(f"Flood error: Please wait {format_duration(e.seconds)}")
- except Exception:
- self.log.exception(f"Fatal error handling command "
- + f"'$cmdprefix {command} {''.join(args)}' from {sender.mxid}")
- return evt.reply("Fatal error while handling command. Check logs for more details.")
-
- # endregion
- # region Command handlers
-
- @command_handler
- async def ping(self, evt):
- if not evt.sender.logged_in:
- return await evt.reply("You're not logged in.")
- me = await evt.sender.client.get_me()
- if me:
- return await evt.reply(f"You're logged in as @{me.username}")
- else:
- return await evt.reply("You're not logged in.")
-
- # region Authentication commands
- @command_handler
- def register(self, evt):
- return evt.reply("Not yet implemented.")
-
- @command_handler
- async def login(self, evt):
- if not evt.is_management:
- return await evt.reply(
- "`login` is a restricted command: you may only run it in management rooms.")
- elif evt.sender.logged_in:
- return await evt.reply("You are already logged in.")
- elif len(evt.args) == 0:
- return await evt.reply("**Usage:** `$cmdprefix+sp login `")
- phone_number = evt.args[0]
- await evt.sender.client.sign_in(phone_number)
- evt.sender.command_status = {
- "next": command_handlers["enter_code"],
- "action": "Login",
- }
- return await evt.reply(f"Login code sent to {phone_number}. Please send the code here.")
-
- @command_handler
- async def enter_code(self, evt):
- if not evt.sender.command_status:
- return await evt.reply(
- "Request a login code first with `$cmdprefix+sp login `")
- elif len(evt.args) == 0:
- return await evt.reply("**Usage:** `$cmdprefix+sp enter_code `")
-
- try:
- user = await evt.sender.client.sign_in(code=evt.args[0])
- asyncio.ensure_future(evt.sender.post_login(user), loop=self.loop)
- evt.sender.command_status = None
- return await evt.reply(f"Successfully logged in as @{user.username}")
- except PhoneNumberUnoccupiedError:
- return await evt.reply("That phone number has not been registered."
- "Please register with `$cmdprefix+sp register `.")
- except PhoneCodeExpiredError:
- return await evt.reply(
- "Phone code expired. Try again with `$cmdprefix+sp login `.")
- except PhoneCodeInvalidError:
- return await evt.reply("Invalid phone code.")
- except PhoneNumberAppSignupForbiddenError:
- return await evt.reply(
- "Your phone number does not allow 3rd party apps to sign in.")
- except PhoneNumberFloodError:
- return await evt.reply(
- "Your phone number has been temporarily blocked for flooding. "
- "The block is usually applied for around a day.")
- except PhoneNumberBannedError:
- return await evt.reply("Your phone number has been banned from Telegram.")
- except SessionPasswordNeededError:
- evt.sender.command_status = {
- "next": command_handlers["enter_password"],
- "action": "Login (password entry)",
- }
- return await evt.reply("Your account has two-factor authentication."
- "Please send your password here.")
- except Exception:
- self.log.exception("Error sending phone code")
- return await evt.reply("Unhandled exception while sending code."
- "Check console for more details.")
-
- @command_handler
- async def enter_password(self, evt):
- if not evt.sender.command_status:
- return await evt.reply(
- "Request a login code first with `$cmdprefix+sp login `")
- elif len(evt.args) == 0:
- return await evt.reply("**Usage:** `$cmdprefix+sp enter_password `")
-
- try:
- user = await evt.sender.client.sign_in(password=evt.args[0])
- asyncio.ensure_future(evt.sender.post_login(user), loop=self.loop)
- evt.sender.command_status = None
- return await evt.reply(f"Successfully logged in as @{user.username}")
- except PasswordHashInvalidError:
- return await evt.reply("Incorrect password.")
- except Exception:
- self.log.exception("Error sending password")
- return await evt.reply("Unhandled exception while sending password. "
- "Check console for more details.")
-
- @command_handler
- async def logout(self, evt):
- if not evt.sender.logged_in:
- return await evt.reply("You're not logged in.")
- if await evt.sender.log_out():
- return await evt.reply("Logged out successfully.")
- return await evt.reply("Failed to log out.")
-
- # endregion
- # region Telegram interaction commands
-
- @command_handler
- async def search(self, evt):
- if len(evt.args) == 0:
- return await evt.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] `")
- elif not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
-
- force_remote = False
- if evt.args[0] in {"-r", "--remote"}:
- force_remote = True
- evt.args.pop(0)
-
- query = " ".join(evt.args)
- if force_remote and len(query) < 5:
- return await evt.reply("Minimum length of query for remote search is 5 characters.")
-
- results, remote = await evt.sender.search(query, force_remote)
-
- if not results:
- if len(query) < 5 and remote:
- return await evt.reply("No local results. "
- "Minimum length of remote query is 5 characters.")
- return await evt.reply("No results 3:")
-
- reply = []
- if remote:
- reply += ["**Results from Telegram server:**", ""]
- else:
- reply += ["**Results in contacts:**", ""]
- reply += [(f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): "
- + f"{puppet.id} ({similarity}% match)")
- for puppet, similarity in results]
-
- # TODO somehow show remote channel results when joining by alias is possible?
-
- return await evt.reply("\n".join(reply))
-
- @command_handler
- async def pm(self, evt):
- if len(evt.args) == 0:
- return await evt.reply("**Usage:** `$cmdprefix+sp pm `")
- elif not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
-
- user = await evt.sender.client.get_entity(evt.args[0])
- if not user:
- return await evt.reply("User not found.")
- elif not isinstance(user, User):
- return await evt.reply("That doesn't seem to be a user.")
- portal = po.Portal.get_by_entity(user, evt.sender.tgid)
- await portal.create_matrix_room(evt.sender, user, [evt.sender.mxid])
- return await evt.reply(
- f"Created private chat room with {pu.Puppet.get_displayname(user, False)}")
-
- @command_handler
- async def invitelink(self, evt):
- if not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
-
- portal = po.Portal.get_by_mxid(evt.room_id)
- if not portal:
- return await evt.reply("This is not a portal room.")
-
- if portal.peer_type == "user":
- return await evt.reply("You can't invite users to private chats.")
-
- try:
- link = await portal.get_invite_link(evt.sender)
- return await evt.reply(f"Invite link to {portal.title}: {link}")
- except ValueError as e:
- return await evt.reply(e.args[0])
- except ChatAdminRequiredError:
- return await evt.reply("You don't have the permission to create an invite link.")
-
- @command_handler
- async def deleteportal(self, evt):
- if not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
- elif not evt.sender.is_admin:
- return await evt.reply("This is command requires administrator privileges.")
-
- portal = po.Portal.get_by_mxid(evt.room_id)
- if not portal:
- return await evt.reply("This is not a portal room.")
-
- for user in await portal.main_intent.get_room_members(portal.mxid):
- if user != portal.main_intent.mxid:
- try:
- await portal.main_intent.kick(portal.mxid, user, "Portal deleted.")
- except MatrixRequestError:
- pass
- await portal.main_intent.leave_room(portal.mxid)
- portal.delete()
-
- @staticmethod
- def _strip_prefix(value, prefixes):
- for prefix in prefixes:
- if value.startswith(prefix):
- return value[len(prefix):]
- return value
-
- @command_handler
- async def join(self, evt):
- if len(evt.args) == 0:
- return await evt.reply("**Usage:** `$cmdprefix+sp join `")
- elif not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
-
- regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:joinchat/)?/(.+)")
- arg = regex.match(evt.args[0])
- if not arg:
- return await evt.reply("That doesn't look like a Telegram invite link.")
- arg = arg.group(1)
- if arg.startswith("joinchat/"):
- invite_hash = arg[len("joinchat/"):]
- try:
- await evt.sender.client(CheckChatInviteRequest(invite_hash))
- except InviteHashInvalidError:
- return await evt.reply("Invalid invite link.")
- except InviteHashExpiredError:
- return await evt.reply("Invite link expired.")
- try:
- updates = evt.sender.client(ImportChatInviteRequest(invite_hash))
- except UserAlreadyParticipantError:
- return await evt.reply("You are already in that chat.")
- else:
- channel = await evt.sender.client.get_entity(arg)
- if not channel:
- return await evt.reply("Channel/supergroup not found.")
- updates = await evt.sender.client(JoinChannelRequest(channel))
- for chat in updates.chats:
- portal = po.Portal.get_by_entity(chat)
- if portal.mxid:
- await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid])
- return await evt.reply(f"Created room for {portal.title}")
- else:
- await portal.invite_matrix([evt.sender.mxid])
- return await evt.reply(f"Invited you to portal of {portal.title}")
-
- @command_handler
- async def create(self, evt):
- type = evt.args[0] if len(evt.args) > 0 else "group"
- if type not in {"chat", "group", "supergroup", "channel"}:
- return await evt.reply(
- "**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`")
- elif not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
-
- if po.Portal.get_by_mxid(evt.room_id):
- return await evt.reply("This is already a portal room.")
-
- state = await self.az.intent.get_room_state(evt.room_id)
- title = None
- about = None
- levels = None
- for event in state:
- if event["type"] == "m.room.name":
- title = event["content"]["name"]
- elif event["type"] == "m.room.topic":
- about = event["content"]["topic"]
- elif event["type"] == "m.room.power_levels":
- levels = event["content"]
- if not title:
- return await evt.reply("Please set a title before creating a Telegram chat.")
- elif (not levels or not levels["users"] or self.az.intent.mxid not in levels["users"] or
- levels["users"][self.az.intent.mxid] < 100):
- return await evt.reply(f"Please give "
- + f"[the bridge bot](https://matrix.to/#/{self.az.intent.mxid})"
- + f" a power level of 100 before creating a Telegram chat.")
- else:
- for user, level in levels["users"].items():
- if level >= 100 and user != self.az.intent.mxid:
- return await evt.reply(
- f"Please make sure only the bridge bot has power level above"
- + f"99 before creating a Telegram chat.\n\n"
- + f"Use power level 95 instead of 100 for admins.")
-
- supergroup = type == "supergroup"
- type = {
- "supergroup": "channel",
- "channel": "channel",
- "chat": "chat",
- "group": "chat",
- }[type]
-
- portal = po.Portal(tgid=None, mxid=evt.room_id, title=title, about=about, peer_type=type)
- try:
- await portal.create_telegram_chat(evt.sender, supergroup=supergroup)
- except ValueError as e:
- return await evt.reply(e.args[0])
- return await evt.reply(f"Telegram chat created. ID: {portal.tgid}")
-
- @command_handler
- async def upgrade(self, evt):
- if not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
-
- portal = po.Portal.get_by_mxid(evt.room_id)
- if not portal:
- return await evt.reply("This is not a portal room.")
- elif portal.peer_type == "channel":
- return await evt.reply("This is already a supergroup or a channel.")
- elif portal.peer_type == "user":
- return await evt.reply("You can't upgrade private chats.")
-
- try:
- await portal.upgrade_telegram_chat(evt.sender)
- return await evt.reply(f"Group upgraded to supergroup. New ID: {portal.tgid}")
- except ChatAdminRequiredError:
- return await evt.reply("You don't have the permission to upgrade this group.")
- except ValueError as e:
- return await evt.reply(e.args[0])
-
- @command_handler
- async def groupname(self, evt):
- if len(evt.args) == 0:
- return await evt.reply("**Usage:** `$cmdprefix+sp groupname `")
- if not evt.sender.logged_in:
- return await evt.reply("This command requires you to be logged in.")
-
- portal = po.Portal.get_by_mxid(evt.room_id)
- if not portal:
- return await evt.reply("This is not a portal room.")
- elif portal.peer_type != "channel":
- return await evt.reply("Only channels and supergroups have usernames.")
-
- try:
- await portal.set_telegram_username(evt.sender,
- evt.args[0] if evt.args[0] != "-" else "")
- if portal.username:
- return await evt.reply(f"Username of channel changed to {portal.username}.")
- else:
- return await evt.reply(f"Channel is now private.")
- except ChatAdminRequiredError:
- return await evt.reply(
- "You don't have the permission to set the username of this channel.")
- except UsernameNotModifiedError:
- if portal.username:
- return await evt.reply("That is already the username of this channel.")
- else:
- return await evt.reply("This channel is already private")
- except UsernameOccupiedError:
- return await evt.reply("That username is already in use.")
- except UsernameInvalidError:
- return await evt.reply("Invalid username")
-
- # endregion
- # region Command-related commands
- @command_handler
- def cancel(self, evt):
- if evt.sender.command_status:
- action = evt.sender.command_status["action"]
- evt.sender.command_status = None
- return evt.reply(f"{action} cancelled.")
- else:
- return evt.reply("No ongoing command.")
-
- @command_handler
- def unknown_command(self, evt):
- return evt.reply("Unknown command. Try `$cmdprefix+sp help` for help.")
-
- @command_handler
- def help(self, evt):
- if evt.is_management:
- management_status = ("This is a management room: prefixing commands "
- "with `$cmdprefix` is not required.\n")
- elif evt.is_portal:
- management_status = ("**This is a portal room**: you must always "
- "prefix commands with `$cmdprefix`.\n"
- "Management commands will not be sent to Telegram.")
- else:
- management_status = ("**This is not a management room**: you must "
- "prefix commands with `$cmdprefix`.\n")
- help = """\n
-#### Generic bridge commands
-**help** - Show this help message.
-**cancel** - Cancel an ongoing action (such as login).
-
-#### Authentication
-**login** <_phone_> - Request an authentication code.
-**logout** - Log out from Telegram.
-**ping** - Check if you're logged into Telegram.
-
-#### Initiating chats
-**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users.
-**pm** <_identifier_> - Open a private chat with the given Telegram user. The
- identifier is either the internal user ID, the username or
- the phone number.
-**join** <_link_> - Join a chat with an invite link.
-**create** [_type_] - Create a Telegram chat of the given type for the current
- Matrix room. The type is either `group`, `supergroup` or
- `channel` (defaults to `group`).
-
-#### Portal management
-**upgrade** - Upgrade a normal Telegram group to a supergroup.
-**invitelink** - Get a Telegram invite link to the current chat.
-**deleteportal** - Forget the current portal room. Only works for group chats; to delete
- a private chat portal, simply leave the room.
-**groupname** <_name_|`-`> - Change the username of a supergroup/channel. To disable, use a dash
- (`-`) as the name.
-"""
- return evt.reply(management_status + help)
-
- # endregion
- # endregion
diff --git a/mautrix_telegram/commands/__init__.py b/mautrix_telegram/commands/__init__.py
new file mode 100644
index 00000000..3e456c9b
--- /dev/null
+++ b/mautrix_telegram/commands/__init__.py
@@ -0,0 +1,2 @@
+from .handler import command_handler, CommandHandler
+from . import clean_rooms, auth, meta, telegram
diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py
new file mode 100644
index 00000000..ceaeeca5
--- /dev/null
+++ b/mautrix_telegram/commands/auth.py
@@ -0,0 +1,118 @@
+# 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 asyncio
+
+from telethon.errors import *
+
+from . import command_handler
+
+
+@command_handler(needs_auth=False)
+async def ping(evt):
+ if not evt.sender.logged_in:
+ return await evt.reply("You're not logged in.")
+ me = await evt.sender.client.get_me()
+ if me:
+ return await evt.reply(f"You're logged in as @{me.username}")
+ else:
+ return await evt.reply("You're not logged in.")
+
+
+@command_handler(needs_auth=False, management_only=True)
+def register(evt):
+ return evt.reply("Not yet implemented.")
+
+
+@command_handler(needs_auth=False, management_only=True)
+async def login(evt):
+ if evt.sender.logged_in:
+ return await evt.reply("You are already logged in.")
+ elif len(evt.args) == 0:
+ return await evt.reply("**Usage:** `$cmdprefix+sp login `")
+ phone_number = evt.args[0]
+ await evt.sender.client.sign_in(phone_number)
+ evt.sender.command_status = {
+ "next": enter_code,
+ "action": "Login",
+ }
+ return await evt.reply(f"Login code sent to {phone_number}. Please send the code here.")
+
+
+@command_handler(needs_auth=False)
+async def enter_code(evt):
+ if len(evt.args) == 0:
+ return await evt.reply("**Usage:** `$cmdprefix+sp enter-code `")
+
+ try:
+ 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
+ return await evt.reply(f"Successfully logged in as @{user.username}")
+ except PhoneNumberUnoccupiedError:
+ return await evt.reply("That phone number has not been registered."
+ "Please register with `$cmdprefix+sp register `.")
+ except PhoneCodeExpiredError:
+ return await evt.reply(
+ "Phone code expired. Try again with `$cmdprefix+sp login `.")
+ except PhoneCodeInvalidError:
+ return await evt.reply("Invalid phone code.")
+ except PhoneNumberAppSignupForbiddenError:
+ return await evt.reply(
+ "Your phone number does not allow 3rd party apps to sign in.")
+ except PhoneNumberFloodError:
+ return await evt.reply(
+ "Your phone number has been temporarily blocked for flooding. "
+ "The block is usually applied for around a day.")
+ except PhoneNumberBannedError:
+ return await evt.reply("Your phone number has been banned from Telegram.")
+ except SessionPasswordNeededError:
+ evt.sender.command_status = {
+ "next": enter_password,
+ "action": "Login (password entry)",
+ }
+ return await evt.reply("Your account has two-factor authentication."
+ "Please send your password here.")
+ except Exception:
+ evt.log.exception("Error sending phone code")
+ return await evt.reply("Unhandled exception while sending code."
+ "Check console for more details.")
+
+
+@command_handler(needs_auth=False)
+async def enter_password(evt):
+ if len(evt.args) == 0:
+ return await evt.reply("**Usage:** `$cmdprefix+sp enter-password `")
+
+ try:
+ 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
+ return await evt.reply(f"Successfully logged in as @{user.username}")
+ except PasswordHashInvalidError:
+ return await evt.reply("Incorrect password.")
+ except Exception:
+ evt.log.exception("Error sending password")
+ return await evt.reply("Unhandled exception while sending password. "
+ "Check console for more details.")
+
+
+@command_handler(needs_auth=False)
+async def logout(evt):
+ if not evt.sender.logged_in:
+ return await evt.reply("You're not logged in.")
+ if await evt.sender.log_out():
+ return await evt.reply("Logged out successfully.")
+ return await evt.reply("Failed to log out.")
diff --git a/mautrix_telegram/commands/clean_rooms.py b/mautrix_telegram/commands/clean_rooms.py
new file mode 100644
index 00000000..05971af4
--- /dev/null
+++ b/mautrix_telegram/commands/clean_rooms.py
@@ -0,0 +1,161 @@
+# 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 .
+from . import command_handler
+from .. import puppet as pu, portal as po
+
+
+async def _find_rooms(intent):
+ management_rooms = []
+ unidentified_rooms = []
+ portals = []
+ empty_portals = []
+
+ rooms = await intent.get_joined_rooms()
+ for room in rooms:
+ portal = po.Portal.get_by_mxid(room)
+ if not portal:
+ members = await intent.get_room_members(room)
+ if len(members) == 2:
+ other_member = members[0] if members[0] != intent.mxid else members[1]
+ if pu.Puppet.get_id_from_mxid(other_member):
+ unidentified_rooms.append(room)
+ else:
+ management_rooms.append((room, other_member))
+ else:
+ unidentified_rooms.append(room)
+ else:
+ members = await portal.get_authenticated_matrix_users()
+ if len(members) == 0:
+ empty_portals.append(portal)
+ else:
+ portals.append(portal)
+
+ return management_rooms, unidentified_rooms, portals, empty_portals
+
+
+@command_handler(needs_admin=True, 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 "
+ "run it in non-management rooms.")
+
+ management_rooms, unidentified_rooms, portals, empty_portals = await _find_rooms(evt.az.intent)
+
+ reply = ["#### Management rooms (M)"]
+ reply += ([f"{n+1}. [M{n+1}](https://matrix.to/#/{room}) (with {other_member}"
+ for n, (room, other_member) in enumerate(management_rooms)]
+ or ["No management rooms found."])
+ reply.append("#### Active portal rooms (A)")
+ reply += ([f"{n+1}. [P{n+1}](https://matrix.to/#/{portal.mxid}) "
+ + f"(to Telegram chat \"{portal.title}\")"
+ for n, portal in enumerate(portals)]
+ or ["No active portal rooms found."])
+ reply.append("#### Unidentified rooms (U)")
+ reply += ([f"{n+1}. [U{n+1}](https://matrix.to/#/{room})"
+ for n, room in enumerate(unidentified_rooms)]
+ or ["No unidentified rooms found."])
+ reply.append("#### Inactive portal rooms (I)")
+ reply += ([f"{n}. [E{n}](https://matrix.to/#/{portal.mxid}) "
+ + f"(to Telegram chat \"{portal.title}\")"
+ for n, portal in enumerate(empty_portals)]
+ or ["No inactive portal rooms found."])
+
+ reply += ["#### Usage",
+ ("To clean the recommended set of rooms (unidentified & inactive portals), "
+ "type `$cmdprefix+sp clean-recommended`"),
+ "",
+ ("To clean other groups of rooms, type `$cmdprefix+sp clean-groups ` "
+ "where `letters` are the first letters of the group names (M, A, U, I)"),
+ "",
+ ("To clean specific rooms, type `$cmdprefix+sp clean-range ` "
+ "where `range` is the range (e.g. `5-21`) prefixed with the first letter of"
+ "the group name."),
+ "",
+ ("Please note that you will have to re-run `$cmdprefix+sp cleanrooms` "
+ "between each use of the commands above.")]
+
+ evt.sender.command_status = {
+ "next": lambda clean_evt: set_rooms_to_clean(clean_evt, management_rooms,
+ unidentified_rooms, portals, empty_portals),
+ "action": "Room cleaning",
+ }
+
+ return await evt.reply("\n".join(reply))
+
+
+async def set_rooms_to_clean(evt, management_rooms, unidentified_rooms, portals, empty_portals):
+ command = evt.args[0]
+ rooms_to_clean = []
+ if command == "clean-recommended":
+ rooms_to_clean = empty_portals + unidentified_rooms
+ elif command == "clean-groups":
+ if len(evt.args) < 2:
+ return await evt.reply("**Usage:** `$cmdprefix+sp clean-groups [M][A][U][I]")
+ groups_to_clean = evt.args[1]
+ if "M" in groups_to_clean:
+ rooms_to_clean += management_rooms
+ if "A" in groups_to_clean:
+ rooms_to_clean += portals
+ if "U" in groups_to_clean:
+ rooms_to_clean += unidentified_rooms
+ if "I" in groups_to_clean:
+ rooms_to_clean += empty_portals
+ elif command == "clean-range":
+ try:
+ range = evt.args[1]
+ group, range = range[0], range[1:]
+ start, end = range.split("-")
+ start, end = int(start), int(end)
+ if group == "M":
+ group = management_rooms
+ elif group == "A":
+ group = portals
+ elif group == "U":
+ group = unidentified_rooms
+ elif group == "I":
+ group = empty_portals
+ else:
+ raise ValueError("Unknown group")
+ rooms_to_clean = group[start - 1:end]
+ except (KeyError, ValueError):
+ return await evt.reply(
+ "**Usage:** `$cmdprefix+sp clean-groups <_M|A|U|I_>")
+ else:
+ return await evt.reply(f"Unknown room cleaning action `{command}`. "
+ + "Use `$cmdprefix+sp cancel` to cancel room "
+ + "cleaning.")
+
+ evt.sender.command_status = {
+ "next": lambda confirm: execute_room_cleanup(confirm, rooms_to_clean),
+ "action": "Room cleaning",
+ }
+ await evt.reply(f"To confirm cleaning up {len(rooms_to_clean)} rooms, type"
+ + "`$cmdprefix+sp confirm-clean`.")
+
+
+async def execute_room_cleanup(evt, rooms_to_clean):
+ if len(evt.args) > 0 and evt.args[0] == "confirm-clean":
+ await evt.reply(f"Cleaning {len(rooms_to_clean)} rooms. "
+ + "This might take a while.")
+ for room in rooms_to_clean:
+ if isinstance(room, po.Portal):
+ await room.cleanup_and_delete()
+ else:
+ await po.Portal.cleanup_room(evt.az.intent, room, type="Room")
+ evt.sender.command_status = None
+ await evt.reply(f"{len(rooms_to_clean)} rooms cleaned up successfully.")
+ else:
+ await evt.reply("Room cleaning cancelled.")
diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py
new file mode 100644
index 00000000..798d9c2b
--- /dev/null
+++ b/mautrix_telegram/commands/handler.py
@@ -0,0 +1,114 @@
+# 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 markdown
+import logging
+
+from telethon.errors import FloodWaitError
+
+command_handlers = {}
+
+
+def command_handler(needs_auth=True, management_only=False, needs_admin=False, name=None):
+ def decorator(func):
+ def wrapper(evt):
+ if management_only and not evt.is_management:
+ return evt.reply(f"`{evt.command}` is a restricted command:"
+ + "you may only run it in management rooms.")
+ elif needs_auth and not evt.sender.logged_in:
+ return evt.reply("This command requires you to be logged in.")
+ elif needs_admin and not evt.sender.is_admin:
+ return evt.reply("This is command requires administrator privileges.")
+ return func(evt)
+
+ command_handlers[name or func.__name__.replace("_", "-")] = wrapper
+ return wrapper
+
+ return decorator
+
+
+class CommandEvent:
+ def __init__(self, handler, room, sender, command, args, is_management, is_portal):
+ self.az = handler.az
+ self.log = handler.log
+ self.loop = handler.loop
+ self.command_prefix = handler.command_prefix
+ self.room_id = room
+ self.sender = sender
+ self.command = command
+ self.args = args
+ self.is_management = is_management
+ self.is_portal = is_portal
+
+ def reply(self, message, allow_html=False, render_markdown=True):
+ message = message.replace("$cmdprefix+sp ",
+ "" if self.is_management else f"{self.command_prefix} ")
+ message = message.replace("$cmdprefix", self.command_prefix)
+ html = None
+ if render_markdown:
+ html = markdown.markdown(message, safe_mode="escape" if allow_html else False)
+ elif allow_html:
+ html = message
+ return self.az.intent.send_notice(self.room_id, message, html=html)
+
+
+def format_duration(seconds):
+ def pluralize(count, singular): return singular if count == 1 else singular + "s"
+
+ def include(count, word): return f"{count} {pluralize(count, word)}" if count > 0 else ""
+
+ minutes, seconds = divmod(seconds, 60)
+ hours, minutes = divmod(minutes, 60)
+ days, hours = divmod(hours, 24)
+ parts = [a for a in [
+ include(days, "day"),
+ include(hours, "hour"),
+ include(minutes, "minute"),
+ include(seconds, "second")] if a]
+ if len(parts) > 2:
+ return "{} and {}".format(", ".join(parts[:-1]), parts[-1])
+ return " and ".join(parts)
+
+
+class CommandHandler:
+ log = logging.getLogger("mau.commands")
+
+ def __init__(self, context):
+ self.az, self.db, self.config, self.loop = context
+ self.command_prefix = self.config["bridge.command_prefix"]
+
+ # region Utility functions for handling commands
+
+ async def handle(self, room, sender, command, args, is_management, is_portal):
+ evt = CommandEvent(self, room, sender, command, args,
+ is_management, is_portal)
+ command = command.lower()
+ try:
+ command = command_handlers[command]
+ except KeyError:
+ if sender.command_status and "next" in sender.command_status:
+ args.insert(0, command)
+ evt.command = ""
+ command = sender.command_status["next"]
+ else:
+ command = command_handlers["unknown_command"]
+ try:
+ await command(evt)
+ except FloodWaitError as e:
+ return evt.reply(f"Flood error: Please wait {format_duration(e.seconds)}")
+ except Exception:
+ self.log.exception(f"Fatal error handling command "
+ + f"{evt.command} {' '.join(args)} from {sender.mxid}")
+ return evt.reply("Fatal error while handling command. Check logs for more details.")
diff --git a/mautrix_telegram/commands/meta.py b/mautrix_telegram/commands/meta.py
new file mode 100644
index 00000000..6f38dc3d
--- /dev/null
+++ b/mautrix_telegram/commands/meta.py
@@ -0,0 +1,74 @@
+# 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 .
+from . import command_handler
+
+
+@command_handler()
+def cancel(evt):
+ if evt.sender.command_status:
+ action = evt.sender.command_status["action"]
+ evt.sender.command_status = None
+ return evt.reply(f"{action} cancelled.")
+ else:
+ return evt.reply("No ongoing command.")
+
+
+@command_handler()
+def unknown_command(evt):
+ return evt.reply("Unknown command. Try `$cmdprefix+sp help` for help.")
+
+
+@command_handler()
+def help(evt):
+ if evt.is_management:
+ management_status = ("This is a management room: prefixing commands "
+ "with `$cmdprefix` is not required.\n")
+ elif evt.is_portal:
+ management_status = ("**This is a portal room**: you must always "
+ "prefix commands with `$cmdprefix`.\n"
+ "Management commands will not be sent to Telegram.")
+ else:
+ management_status = ("**This is not a management room**: you must "
+ "prefix commands with `$cmdprefix`.\n")
+ help = """\n
+#### Generic bridge commands
+**help** - Show this help message.
+**cancel** - Cancel an ongoing action (such as login).
+
+#### Authentication
+**login** <_phone_> - Request an authentication code.
+**logout** - Log out from Telegram.
+**ping** - Check if you're logged into Telegram.
+
+#### Initiating chats
+**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users.
+**pm** <_identifier_> - Open a private chat with the given Telegram user. The
+ identifier is either the internal user ID, the username or
+ the phone number.
+**join** <_link_> - Join a chat with an invite link.
+**create** [_type_] - Create a Telegram chat of the given type for the current
+ Matrix room. The type is either `group`, `supergroup` or
+ `channel` (defaults to `group`).
+
+#### Portal management
+**upgrade** - Upgrade a normal Telegram group to a supergroup.
+**invite-link** - Get a Telegram invite link to the current chat.
+**delete-portal** - Forget the current portal room. Only works for group chats; to delete
+ a private chat portal, simply leave the room.
+**group-name** <_name_|`-`> - Change the username of a supergroup/channel. To disable, use a dash
+ (`-`) as the name.
+"""
+ return evt.reply(management_status + help)
diff --git a/mautrix_telegram/commands/telegram.py b/mautrix_telegram/commands/telegram.py
new file mode 100644
index 00000000..6b157d63
--- /dev/null
+++ b/mautrix_telegram/commands/telegram.py
@@ -0,0 +1,260 @@
+# 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 .
+from telethon.errors import *
+from telethon.tl.types import User as TLUser
+from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest
+from telethon.tl.functions.channels import JoinChannelRequest
+
+from .. import puppet as pu, portal as po
+from . import command_handler
+
+
+@command_handler()
+async def search(evt):
+ if len(evt.args) == 0:
+ return await evt.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] `")
+
+ force_remote = False
+ if evt.args[0] in {"-r", "--remote"}:
+ force_remote = True
+ evt.args.pop(0)
+
+ query = " ".join(evt.args)
+ if force_remote and len(query) < 5:
+ return await evt.reply("Minimum length of query for remote search is 5 characters.")
+
+ results, remote = await evt.sender.search(query, force_remote)
+
+ if not results:
+ if len(query) < 5 and remote:
+ return await evt.reply("No local results. "
+ "Minimum length of remote query is 5 characters.")
+ return await evt.reply("No results 3:")
+
+ reply = []
+ if remote:
+ reply += ["**Results from Telegram server:**", ""]
+ else:
+ reply += ["**Results in contacts:**", ""]
+ reply += [(f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): "
+ + f"{puppet.id} ({similarity}% match)")
+ for puppet, similarity in results]
+
+ # TODO somehow show remote channel results when joining by alias is possible?
+
+ return await evt.reply("\n".join(reply))
+
+
+@command_handler()
+async def pm(evt):
+ if len(evt.args) == 0:
+ return await evt.reply("**Usage:** `$cmdprefix+sp pm `")
+
+ user = await evt.sender.client.get_entity(evt.args[0])
+ if not user:
+ return await evt.reply("User not found.")
+ elif not isinstance(user, TLUser):
+ return await evt.reply("That doesn't seem to be a user.")
+ portal = po.Portal.get_by_entity(user, evt.sender.tgid)
+ await portal.create_matrix_room(evt.sender, user, [evt.sender.mxid])
+ return await evt.reply(
+ f"Created private chat room with {pu.Puppet.get_displayname(user, False)}")
+
+
+@command_handler()
+async def invite_link(evt):
+ portal = po.Portal.get_by_mxid(evt.room_id)
+ if not portal:
+ return await evt.reply("This is not a portal room.")
+
+ if portal.peer_type == "user":
+ return await evt.reply("You can't invite users to private chats.")
+
+ try:
+ link = await portal.get_invite_link(evt.sender)
+ return await evt.reply(f"Invite link to {portal.title}: {link}")
+ except ValueError as e:
+ return await evt.reply(e.args[0])
+ except ChatAdminRequiredError:
+ return await evt.reply("You don't have the permission to create an invite link.")
+
+
+@command_handler(needs_admin=True)
+async def delete_portal(evt):
+ room_id = evt.args[0] if len(evt.args) > 0 else evt.room_id
+
+ portal = po.Portal.get_by_mxid(room_id)
+ if not portal:
+ that_this = "This" if room_id == evt.room_id else "That"
+ return await evt.reply(f"{that_this} is not a portal room.")
+
+ async def post_confirm(_, confirm):
+ evt.sender.command_status = None
+ if len(confirm.args) > 0 and confirm.args[0] == "confirm-delete":
+ await portal.cleanup_and_delete()
+ if confirm.room_id != room_id:
+ return await confirm.reply("Portal successfully deleted.")
+ else:
+ return await confirm.reply("Portal deletion cancelled.")
+
+ evt.sender.command_status = {
+ "next": post_confirm,
+ "action": "Portal deletion",
+ }
+ return await evt.reply("Please confirm deletion of portal "
+ + f"[{room_id}](https://matrix.to/#/{room_id}) "
+ + f"to Telegram chat \"{portal.title}\" "
+ + "by typing `$cmdprefix+sp confirm-delete`")
+
+
+@command_handler()
+async def join(evt):
+ if len(evt.args) == 0:
+ return await evt.reply("**Usage:** `$cmdprefix+sp join `")
+
+ regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:joinchat/)?/(.+)")
+ arg = regex.match(evt.args[0])
+ if not arg:
+ return await evt.reply("That doesn't look like a Telegram invite link.")
+ arg = arg.group(1)
+ if arg.startswith("joinchat/"):
+ invite_hash = arg[len("joinchat/"):]
+ try:
+ await evt.sender.client(CheckChatInviteRequest(invite_hash))
+ except InviteHashInvalidError:
+ return await evt.reply("Invalid invite link.")
+ except InviteHashExpiredError:
+ return await evt.reply("Invite link expired.")
+ try:
+ updates = evt.sender.client(ImportChatInviteRequest(invite_hash))
+ except UserAlreadyParticipantError:
+ return await evt.reply("You are already in that chat.")
+ else:
+ channel = await evt.sender.client.get_entity(arg)
+ if not channel:
+ return await evt.reply("Channel/supergroup not found.")
+ updates = await evt.sender.client(JoinChannelRequest(channel))
+ for chat in updates.chats:
+ portal = po.Portal.get_by_entity(chat)
+ if portal.mxid:
+ await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid])
+ return await evt.reply(f"Created room for {portal.title}")
+ else:
+ await portal.invite_matrix([evt.sender.mxid])
+ return await evt.reply(f"Invited you to portal of {portal.title}")
+
+
+@command_handler()
+async def create(evt):
+ type = evt.args[0] if len(evt.args) > 0 else "group"
+ if type not in {"chat", "group", "supergroup", "channel"}:
+ return await evt.reply(
+ "**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`")
+
+ if po.Portal.get_by_mxid(evt.room_id):
+ return await evt.reply("This is already a portal room.")
+
+ state = await evt.az.intent.get_room_state(evt.room_id)
+ title = None
+ about = None
+ levels = None
+ for event in state:
+ if event["type"] == "m.room.name":
+ title = event["content"]["name"]
+ elif event["type"] == "m.room.topic":
+ about = event["content"]["topic"]
+ elif event["type"] == "m.room.power_levels":
+ levels = event["content"]
+ if not title:
+ return await evt.reply("Please set a title before creating a Telegram chat.")
+ elif (not levels or not levels["users"] or evt.az.intent.mxid not in levels["users"] or
+ levels["users"][evt.az.intent.mxid] < 100):
+ return await evt.reply(f"Please give "
+ + f"[the bridge bot](https://matrix.to/#/{evt.az.intent.mxid})"
+ + f" a power level of 100 before creating a Telegram chat.")
+ else:
+ for user, level in levels["users"].items():
+ if level >= 100 and user != evt.az.intent.mxid:
+ return await evt.reply(
+ f"Please make sure only the bridge bot has power level above"
+ + f"99 before creating a Telegram chat.\n\n"
+ + f"Use power level 95 instead of 100 for admins.")
+
+ supergroup = type == "supergroup"
+ type = {
+ "supergroup": "channel",
+ "channel": "channel",
+ "chat": "chat",
+ "group": "chat",
+ }[type]
+
+ portal = po.Portal(tgid=None, mxid=evt.room_id, title=title, about=about, peer_type=type)
+ try:
+ await portal.create_telegram_chat(evt.sender, supergroup=supergroup)
+ except ValueError as e:
+ return await evt.reply(e.args[0])
+ return await evt.reply(f"Telegram chat created. ID: {portal.tgid}")
+
+
+@command_handler()
+async def upgrade(evt):
+ portal = po.Portal.get_by_mxid(evt.room_id)
+ if not portal:
+ return await evt.reply("This is not a portal room.")
+ elif portal.peer_type == "channel":
+ return await evt.reply("This is already a supergroup or a channel.")
+ elif portal.peer_type == "user":
+ return await evt.reply("You can't upgrade private chats.")
+
+ try:
+ await portal.upgrade_telegram_chat(evt.sender)
+ return await evt.reply(f"Group upgraded to supergroup. New ID: {portal.tgid}")
+ except ChatAdminRequiredError:
+ return await evt.reply("You don't have the permission to upgrade this group.")
+ except ValueError as e:
+ return await evt.reply(e.args[0])
+
+
+@command_handler()
+async def group_name(evt):
+ if len(evt.args) == 0:
+ return await evt.reply("**Usage:** `$cmdprefix+sp group-name `")
+
+ portal = po.Portal.get_by_mxid(evt.room_id)
+ if not portal:
+ return await evt.reply("This is not a portal room.")
+ elif portal.peer_type != "channel":
+ return await evt.reply("Only channels and supergroups have usernames.")
+
+ try:
+ await portal.set_telegram_username(evt.sender,
+ evt.args[0] if evt.args[0] != "-" else "")
+ if portal.username:
+ return await evt.reply(f"Username of channel changed to {portal.username}.")
+ else:
+ return await evt.reply(f"Channel is now private.")
+ except ChatAdminRequiredError:
+ return await evt.reply(
+ "You don't have the permission to set the username of this channel.")
+ except UsernameNotModifiedError:
+ if portal.username:
+ return await evt.reply("That is already the username of this channel.")
+ else:
+ return await evt.reply("This channel is already private")
+ except UsernameOccupiedError:
+ return await evt.reply("That username is already in use.")
+ except UsernameInvalidError:
+ return await evt.reply("Invalid username")
diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py
index b15ebb6d..6351897a 100644
--- a/mautrix_telegram/portal.py
+++ b/mautrix_telegram/portal.py
@@ -29,6 +29,7 @@ from telethon.tl.functions.messages import *
from telethon.tl.functions.channels import *
from telethon.errors.rpc_error_list import *
from telethon.tl.types import *
+from mautrix_appservice import MatrixRequestError
from .db import Portal as DBPortal, Message as DBMessage
from . import puppet as p, user as u, formatter
@@ -355,6 +356,31 @@ class Portal:
return link.link
+ async def get_authenticated_matrix_users(self):
+ members = await self.main_intent.get_room_members(self.mxid)
+ authenticated = []
+ 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)
+ if user.has_full_access:
+ authenticated.append(user)
+ return authenticated
+
+ @staticmethod
+ async def cleanup_room(intent, room_id, type="Portal"):
+ for user in await intent.get_room_members(room_id):
+ if user != intent.mxid:
+ try:
+ await intent.kick(room_id, user, f"{type} deleted.")
+ except MatrixRequestError:
+ pass
+ await intent.leave_room(room_id)
+
+ async def cleanup_and_delete(self):
+ await self.cleanup_room(self.main_intent, self.mxid)
+ self.delete()
+
# endregion
# region Matrix event handling
diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py
index ba37e1ae..04ab26c4 100644
--- a/mautrix_telegram/user.py
+++ b/mautrix_telegram/user.py
@@ -26,6 +26,7 @@ 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__
+
config = None
@@ -47,7 +48,16 @@ class User:
self.command_status = None
self.connected = False
- self.client = None
+ 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)
self.is_admin = self.mxid in config.get("bridge.admins", [])
@@ -104,16 +114,6 @@ class User:
# region Telegram connection management
async def start(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)
self.connected = await self.client.connect()
if self.logged_in:
asyncio.ensure_future(self.post_login(), loop=self.loop)