diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml
new file mode 100644
index 00000000..ef4ab384
--- /dev/null
+++ b/.github/workflows/python-lint.yml
@@ -0,0 +1,18 @@
+name: Python lint
+
+on: [push, pull_request]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.10"
+ - uses: isort/isort-action@master
+ with:
+ sortPaths: "./mautrix_telegram"
+ - uses: psf/black@21.12b0
+ with:
+ src: "./mautrix_telegram"
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 6da06937..00000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,16 +0,0 @@
-[settings]
-line_length=99
-indent=4
-
-multi_line_output=5
-
-sections=FUTURE,STDLIB,THIRDPARTY,TELETHON,MAUTRIX,FIRSTPARTY,LOCALFOLDER
-no_lines_before=LOCALFOLDER
-default_section=FIRSTPARTY
-
-known_thirdparty=aiohttp,sqlalchemy,alembic,commonmark,ruamel.yaml,PIL,moviepy,prometheus_client,yarl,mako,pkg_resources
-known_telethon=telethon,alchemysession,cryptg
-known_mautrix=mautrix
-
-balanced_wrapping=True
-length_sort=True
diff --git a/README.md b/README.md
index df64b5c6..2cda33c5 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,8 @@
[](LICENSE)
[](https://github.com/mautrix/telegram/releases)
[](https://mau.dev/mautrix/telegram/container_registry)
+[](https://github.com/psf/black)
+[](https://pycqa.github.io/isort/)
A Matrix-Telegram hybrid puppeting/relaybot bridge.
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index 9a92366b..48b7fc09 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -19,12 +19,9 @@ from typing import Any
from telethon import __version__ as __telethon_version__
-from mautrix.types import UserID, RoomID
from mautrix.bridge import Bridge
+from mautrix.types import RoomID, UserID
-from .web.provisioning import ProvisioningAPI
-from .web.public import PublicBridgeWebsite
-from .abstract_user import AbstractUser
from .bot import Bot
from .config import Config
from .db import init as init_db, upgrade_table
@@ -32,7 +29,11 @@ from .matrix import MatrixHandler
from .portal import Portal
from .puppet import Puppet
from .user import User
-from .version import version, linkified_version
+from .version import linkified_version, version
+from .web.provisioning import ProvisioningAPI
+from .web.public import PublicBridgeWebsite
+
+from .abstract_user import AbstractUser # isort: skip
class TelegramBridge(Bridge):
diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py
index f44e3182..bac04640 100644
--- a/mautrix_telegram/abstract_user.py
+++ b/mautrix_telegram/abstract_user.py
@@ -15,45 +15,81 @@
# along with this program. If not, see .
from __future__ import annotations
-from typing import Type, Any, Union, TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, Type, Union
from abc import ABC, abstractmethod
-import platform
import asyncio
import logging
+import platform
import time
+from telethon.network import (
+ Connection,
+ ConnectionTcpFull,
+ ConnectionTcpMTProxyRandomizedIntermediate,
+)
from telethon.sessions import Session
-from telethon.network import (ConnectionTcpMTProxyRandomizedIntermediate, ConnectionTcpFull,
- Connection)
-from telethon.tl.patched import MessageService, Message
+from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (
- Channel, Chat, MessageActionChannelMigrateFrom, PeerUser, TypeUpdate, UpdatePinnedMessages,
- UpdatePinnedChannelMessages, UpdateChatParticipantAdmin, UpdateChatParticipants, PeerChat,
- UpdateChatUserTyping, UpdateDeleteChannelMessages, UpdateNewMessage, UpdateDeleteMessages,
- UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
- UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
- UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox,
- UpdateReadChannelInbox, MessageEmpty, UpdateFolderPeers, UpdatePinnedDialogs,
- UpdateNotifySettings, UpdateChannelUserTyping)
+ Channel,
+ Chat,
+ MessageActionChannelMigrateFrom,
+ MessageEmpty,
+ PeerChat,
+ PeerUser,
+ TypeUpdate,
+ UpdateChannelUserTyping,
+ UpdateChatParticipantAdmin,
+ UpdateChatParticipants,
+ UpdateChatUserTyping,
+ UpdateDeleteChannelMessages,
+ UpdateDeleteMessages,
+ UpdateEditChannelMessage,
+ UpdateEditMessage,
+ UpdateFolderPeers,
+ UpdateNewChannelMessage,
+ UpdateNewMessage,
+ UpdateNotifySettings,
+ UpdatePinnedChannelMessages,
+ UpdatePinnedDialogs,
+ UpdatePinnedMessages,
+ UpdateReadChannelInbox,
+ UpdateReadHistoryInbox,
+ UpdateReadHistoryOutbox,
+ UpdateShortChatMessage,
+ UpdateShortMessage,
+ UpdateUserName,
+ UpdateUserPhoto,
+ UpdateUserStatus,
+ UpdateUserTyping,
+ User,
+ UserStatusOffline,
+ UserStatusOnline,
+)
-from mautrix.types import UserID, PresenceState
-from mautrix.errors import MatrixError
from mautrix.appservice import AppService
+from mautrix.errors import MatrixError
+from mautrix.types import PresenceState, UserID
from mautrix.util.logging import TraceLogger
-from mautrix.util.opt_prometheus import Histogram, Counter
+from mautrix.util.opt_prometheus import Counter, Histogram
-from . import portal as po, puppet as pu, __version__
-from .db import Message as DBMessage, PgSession
-from .types import TelegramID
-from .tgclient import MautrixTelegramClient
+from . import __version__, portal as po, puppet as pu
from .config import Config
+from .db import Message as DBMessage, PgSession
+from .tgclient import MautrixTelegramClient
+from .types import TelegramID
if TYPE_CHECKING:
- from .bot import Bot
from .__main__ import TelegramBridge
+ from .bot import Bot
-UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
- UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage]
+UpdateMessage = Union[
+ UpdateShortChatMessage,
+ UpdateShortMessage,
+ UpdateNewChannelMessage,
+ UpdateNewMessage,
+ UpdateEditMessage,
+ UpdateEditChannelMessage,
+]
UpdateMessageContent = Union[
UpdateShortMessage, UpdateShortChatMessage, Message, MessageService, MessageEmpty
]
@@ -74,9 +110,9 @@ class AbstractUser(ABC):
loop: asyncio.AbstractEventLoop = None
log: TraceLogger
az: AppService
- bridge: 'TelegramBridge'
+ bridge: "TelegramBridge"
config: Config
- relaybot: 'Bot'
+ relaybot: "Bot"
ignore_incoming_bot_events: bool = True
max_deletions: int = 10
@@ -113,11 +149,13 @@ class AbstractUser(ABC):
def _proxy_settings(self) -> tuple[Type[Connection], tuple[Any, ...] | None]:
proxy_type = self.config["telegram.proxy.type"].lower()
connection = ConnectionTcpFull
- connection_data = (self.config["telegram.proxy.address"],
- self.config["telegram.proxy.port"],
- self.config["telegram.proxy.rdns"],
- self.config["telegram.proxy.username"],
- self.config["telegram.proxy.password"])
+ connection_data = (
+ self.config["telegram.proxy.address"],
+ self.config["telegram.proxy.port"],
+ self.config["telegram.proxy.rdns"],
+ self.config["telegram.proxy.username"],
+ self.config["telegram.proxy.password"],
+ )
if proxy_type == "disabled":
connection_data = None
elif proxy_type == "socks4":
@@ -133,7 +171,7 @@ class AbstractUser(ABC):
return connection, connection_data
@classmethod
- def init_cls(cls, bridge: 'TelegramBridge') -> None:
+ def init_cls(cls, bridge: "TelegramBridge") -> None:
cls.bridge = bridge
cls.config = bridge.config
cls.loop = bridge.loop
@@ -146,9 +184,11 @@ class AbstractUser(ABC):
session = await PgSession.get(self.name)
if self.config["telegram.server.enabled"]:
- session.set_dc(self.config["telegram.server.dc"],
- self.config["telegram.server.ip"],
- self.config["telegram.server.port"])
+ session.set_dc(
+ self.config["telegram.server.dc"],
+ self.config["telegram.server.ip"],
+ self.config["telegram.server.port"],
+ )
if self.is_relaybot:
base_logger = logging.getLogger("telethon.relaybot")
@@ -164,16 +204,15 @@ class AbstractUser(ABC):
self.client = MautrixTelegramClient(
session=session,
-
api_id=self.config["telegram.api_id"],
api_hash=self.config["telegram.api_hash"],
-
app_version=__version__ if appversion == "auto" else appversion,
- system_version=(MautrixTelegramClient.__version__
- if sysversion == "auto" else sysversion),
- device_model=(f"{platform.system()} {platform.release()}"
- if device == "auto" else device),
-
+ system_version=(
+ MautrixTelegramClient.__version__ if sysversion == "auto" else sysversion
+ ),
+ device_model=(
+ f"{platform.system()} {platform.release()}" if device == "auto" else device
+ ),
timeout=self.config["telegram.connection.timeout"],
connection_retries=self.config["telegram.connection.retries"],
retry_delay=self.config["telegram.connection.retry_delay"],
@@ -182,9 +221,8 @@ class AbstractUser(ABC):
connection=connection,
proxy=proxy,
raise_last_call_error=True,
-
loop=self.loop,
- base_logger=base_logger
+ base_logger=base_logger,
)
self.client.add_event_handler(self._update_catch)
@@ -221,13 +259,16 @@ class AbstractUser(ABC):
raise NotImplementedError()
async def is_logged_in(self) -> bool:
- return (self.client and self.client.is_connected()
- and await self.client.is_user_authorized())
+ return (
+ self.client and self.client.is_connected() and await self.client.is_user_authorized()
+ )
async def has_full_access(self, allow_bot: bool = False) -> bool:
- return (self.puppet_whitelisted
- and (not self.is_bot or allow_bot)
- and await self.is_logged_in())
+ return (
+ self.puppet_whitelisted
+ and (not self.is_bot or allow_bot)
+ and await self.is_logged_in()
+ )
async def start(self, delete_unless_authenticated: bool = False) -> AbstractUser:
if not self.client:
@@ -240,8 +281,10 @@ class AbstractUser(ABC):
if self.connected:
return self
if even_if_no_session or await PgSession.has(self.mxid):
- self.log.debug("Starting client due to ensure_started"
- f"(even_if_no_session={even_if_no_session})")
+ self.log.debug(
+ "Starting client due to ensure_started"
+ f"(even_if_no_session={even_if_no_session})"
+ )
await self.start(delete_unless_authenticated=not even_if_no_session)
return self
@@ -253,8 +296,17 @@ class AbstractUser(ABC):
async def _update(self, update: TypeUpdate) -> None:
asyncio.create_task(self._handle_entity_updates(getattr(update, "_entities", {})))
- if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
- UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)):
+ if isinstance(
+ update,
+ (
+ UpdateShortChatMessage,
+ UpdateShortMessage,
+ UpdateNewChannelMessage,
+ UpdateNewMessage,
+ UpdateEditMessage,
+ UpdateEditChannelMessage,
+ ),
+ ):
await self.update_message(update)
elif isinstance(update, UpdateDeleteMessages):
await self.delete_message(update)
@@ -302,8 +354,9 @@ class AbstractUser(ABC):
else:
portal = await po.Portal.get_by_tgid(TelegramID(update.channel_id))
if portal and portal.mxid:
- await portal.receive_telegram_pin_ids(update.messages, self.tgid,
- remove=not update.pinned)
+ await portal.receive_telegram_pin_ids(
+ update.messages, self.tgid, remove=not update.pinned
+ )
@staticmethod
async def update_participants(update: UpdateChatParticipants) -> None:
@@ -323,8 +376,9 @@ class AbstractUser(ABC):
return
# We check that these are user read receipts, so tg_space is always the user ID.
- message = await DBMessage.get_one_by_tgid(TelegramID(update.max_id), self.tgid,
- edit_index=-1)
+ message = await DBMessage.get_one_by_tgid(
+ TelegramID(update.max_id), self.tgid, edit_index=-1
+ )
if not message:
return
@@ -354,8 +408,9 @@ class AbstractUser(ABC):
return
tg_space = portal.tgid if portal.peer_type == "channel" else self.tgid
- message = await DBMessage.get_one_by_tgid(TelegramID(update.max_id), tg_space,
- edit_index=-1)
+ message = await DBMessage.get_one_by_tgid(
+ TelegramID(update.max_id), tg_space, edit_index=-1
+ )
if not message:
return
@@ -400,8 +455,9 @@ class AbstractUser(ABC):
try:
users = (entity for entity in entities.values() if isinstance(entity, User))
puppets = ((await pu.Puppet.get_by_tgid(TelegramID(user.id)), user) for user in users)
- await asyncio.gather(*[puppet.try_update_info(self, info)
- async for puppet, info in puppets if puppet])
+ await asyncio.gather(
+ *[puppet.try_update_info(self, info) async for puppet, info in puppets if puppet]
+ )
except Exception:
self.log.exception("Failed to handle entity updates")
@@ -441,8 +497,15 @@ class AbstractUser(ABC):
TelegramID(update.user_id), tg_receiver=self.tgid, peer_type="user"
)
sender = await pu.Puppet.get_by_tgid(self.tgid if update.out else update.user_id)
- elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage,
- UpdateEditMessage, UpdateEditChannelMessage)):
+ elif isinstance(
+ update,
+ (
+ UpdateNewMessage,
+ UpdateNewChannelMessage,
+ UpdateEditMessage,
+ UpdateEditChannelMessage,
+ ),
+ ):
update = update.message
if isinstance(update, MessageEmpty):
return update, None, None
@@ -454,8 +517,9 @@ class AbstractUser(ABC):
else:
sender = None
else:
- self.log.warning("Unexpected message type in User#get_message_details: "
- f"{type(update)}")
+ self.log.warning(
+ f"Unexpected message type in User#get_message_details: {type(update)}"
+ )
return update, None, None
return update, sender, portal
@@ -509,26 +573,39 @@ class AbstractUser(ABC):
self.log.debug(f"Ignoring private message to bot from {sender.id}")
return
elif not portal.mxid and self.config["bridge.relaybot.ignore_unbridged_group_chat"]:
- self.log.debug("Ignoring message received by bot"
- f" in unbridged chat {portal.tgid_log}")
+ self.log.debug(
+ f"Ignoring message received by bot in unbridged chat {portal.tgid_log}"
+ )
return
- if ((self.ignore_incoming_bot_events and self.relaybot
- and sender and sender.id == self.relaybot.tgid)):
- self.log.debug(f"Ignoring relaybot-sent message %s to %s", update.id, portal.tgid_log)
+ if (
+ self.ignore_incoming_bot_events
+ and self.relaybot
+ and sender
+ and sender.id == self.relaybot.tgid
+ ):
+ self.log.debug("Ignoring relaybot-sent message %s to %s", update.id, portal.tgid_log)
return
await portal.backfill_lock.wait(f"update {update.id}")
if isinstance(update, MessageService):
if isinstance(update.action, MessageActionChannelMigrateFrom):
- self.log.trace(f"Received %s in %s by %d, unregistering portal...",
- update.action, portal.tgid_log, sender.id)
+ self.log.trace(
+ "Received %s in %s by %d, unregistering portal...",
+ update.action,
+ portal.tgid_log,
+ sender.id,
+ )
await self.unregister_portal(update.action.chat_id, update.action.chat_id)
await self.register_portal(portal)
return
- self.log.trace("Handling action %s to %s by %d", update.action, portal.tgid_log,
- (sender.id if sender else 0))
+ self.log.trace(
+ "Handling action %s to %s by %d",
+ update.action,
+ portal.tgid_log,
+ (sender.id if sender else 0),
+ )
return await portal.handle_telegram_action(self, sender, update)
if isinstance(original_update, (UpdateEditMessage, UpdateEditChannelMessage)):
diff --git a/mautrix_telegram/bot.py b/mautrix_telegram/bot.py
index ea2e67a7..f42dcf50 100644
--- a/mautrix_telegram/bot.py
+++ b/mautrix_telegram/bot.py
@@ -13,26 +13,40 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Awaitable, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
+from typing import Awaitable, Callable, Dict, List, Optional, Tuple
import logging
+from telethon.errors import ChannelInvalidError, ChannelPrivateError
+from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest
+from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest
from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (
- ChannelParticipantAdmin, ChannelParticipantCreator, ChatForbidden, ChatParticipantAdmin,
- ChatParticipantCreator, InputChannel, InputUser, MessageActionChatAddUser, PeerUser,
- MessageActionChatDeleteUser, MessageEntityBotCommand, PeerChannel, PeerChat, TypePeer,
- UpdateNewChannelMessage, UpdateNewMessage, MessageActionChatMigrateTo, User)
-from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest
-from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest
-from telethon.errors import ChannelInvalidError, ChannelPrivateError
+ ChannelParticipantAdmin,
+ ChannelParticipantCreator,
+ ChatForbidden,
+ ChatParticipantAdmin,
+ ChatParticipantCreator,
+ InputChannel,
+ InputUser,
+ MessageActionChatAddUser,
+ MessageActionChatDeleteUser,
+ MessageActionChatMigrateTo,
+ MessageEntityBotCommand,
+ PeerChannel,
+ PeerChat,
+ PeerUser,
+ TypePeer,
+ UpdateNewChannelMessage,
+ UpdateNewMessage,
+ User,
+)
from mautrix.types import UserID
+from . import portal as po, puppet as pu, user as u
from .abstract_user import AbstractUser
from .db import BotChat
from .types import TelegramID
-from . import puppet as pu, portal as po, user as u
-
ReplyFunc = Callable[[str], Awaitable[Message]]
@@ -60,8 +74,9 @@ class Bot(AbstractUser):
self.is_bot = True
self.chats = {}
self.tg_whitelist = []
- self.whitelist_group_admins = (self.config["bridge.relaybot.whitelist_group_admins"]
- or False)
+ self.whitelist_group_admins = (
+ self.config["bridge.relaybot.whitelist_group_admins"] or False
+ )
self._me_info = None
self._me_mxid = None
@@ -83,7 +98,7 @@ class Bot(AbstractUser):
if isinstance(user_id, int):
self.tg_whitelist.append(user_id)
- async def start(self, delete_unless_authenticated: bool = False) -> 'Bot':
+ async def start(self, delete_unless_authenticated: bool = False) -> "Bot":
self.chats = {chat.id: chat.type for chat in await BotChat.all()}
await super().start(delete_unless_authenticated)
if not await self.is_logged_in():
@@ -104,9 +119,11 @@ class Bot(AbstractUser):
if isinstance(chat, ChatForbidden) or chat.left or chat.deactivated:
await self.remove_chat(TelegramID(chat.id))
- channel_ids = [InputChannel(chat_id, 0)
- for chat_id, chat_type in self.chats.items()
- if chat_type == "channel"]
+ channel_ids = [
+ InputChannel(chat_id, 0)
+ for chat_id, chat_type in self.chats.items()
+ if chat_type == "channel"
+ ]
for channel_id in channel_ids:
try:
await self.client(GetChannelsRequest([channel_id]))
@@ -143,7 +160,9 @@ class Bot(AbstractUser):
if self.whitelist_group_admins:
if isinstance(chat, PeerChannel):
p = await self.client(GetParticipantRequest(chat, tgid))
- return isinstance(p.participant, (ChannelParticipantCreator, ChannelParticipantAdmin))
+ return isinstance(
+ p.participant, (ChannelParticipantCreator, ChannelParticipantAdmin)
+ )
elif isinstance(chat, PeerChat):
chat = await self.client(GetFullChatRequest(chat.chat_id))
participants = chat.full_chat.participants.participants
@@ -170,27 +189,29 @@ class Bot(AbstractUser):
if portal.mxid:
if portal.username:
return await reply(
- f"Portal is public: [{portal.alias}](https://matrix.to/#/{portal.alias})")
+ f"Portal is public: [{portal.alias}](https://matrix.to/#/{portal.alias})"
+ )
else:
- return await reply(
- "Portal is not public. Use `/invite ` to get an invite.")
+ return await reply("Portal is not public. Use `/invite ` to get an invite.")
- async def handle_command_invite(self, portal: po.Portal, reply: ReplyFunc,
- mxid_input: UserID) -> Message:
+ async def handle_command_invite(
+ self, portal: po.Portal, reply: ReplyFunc, mxid_input: UserID
+ ) -> Message:
if len(mxid_input) == 0:
return await reply("Usage: `/invite `")
elif not portal.mxid:
- return await reply("Portal does not have Matrix room. "
- "Create one with /portal first.")
- if mxid_input[0] != '@' or mxid_input.find(':') < 2:
+ return await reply("Portal does not have Matrix room. Create one with /portal first.")
+ if mxid_input[0] != "@" or mxid_input.find(":") < 2:
return await reply("That doesn't look like a Matrix ID.")
user = await u.User.get_and_start_by_mxid(mxid_input)
if not user.relaybot_whitelisted:
return await reply("That user is not whitelisted to use the bridge.")
elif await user.is_logged_in():
displayname = f"@{user.tg_username}" if user.tg_username else user.displayname
- return await reply("That user seems to be logged in. "
- f"Just invite [{displayname}](tg://user?id={user.tgid})")
+ return await reply(
+ "That user seems to be logged in. "
+ f"Just invite [{displayname}](tg://user?id={user.tgid})"
+ )
else:
await portal.invite_to_matrix(user.mxid)
return await reply(f"Invited `{user.mxid}` to the portal.")
@@ -251,7 +272,7 @@ class Bot(AbstractUser):
await self.handle_command_portal(portal, reply)
elif is_invite_cmd:
try:
- mxid = text[text.index(" ") + 1:]
+ mxid = text[text.index(" ") + 1 :]
except ValueError:
mxid = ""
await self.handle_command_invite(portal, reply, mxid_input=UserID(mxid))
@@ -283,10 +304,13 @@ class Bot(AbstractUser):
await self.handle_service_message(update.message)
return False
- is_command = (isinstance(update.message, Message)
- and update.message.entities and len(update.message.entities) > 0
- and isinstance(update.message.entities[0], MessageEntityBotCommand)
- and update.message.entities[0].offset == 0)
+ is_command = (
+ isinstance(update.message, Message)
+ and update.message.entities
+ and len(update.message.entities) > 0
+ and isinstance(update.message.entities[0], MessageEntityBotCommand)
+ and update.message.entities[0].offset == 0
+ )
if is_command:
await self.handle_command(update.message)
return False
diff --git a/mautrix_telegram/commands/__init__.py b/mautrix_telegram/commands/__init__.py
index 3693689c..8182877e 100644
--- a/mautrix_telegram/commands/__init__.py
+++ b/mautrix_telegram/commands/__init__.py
@@ -1,8 +1,26 @@
-from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent,
- SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT,
- SECTION_MISC, SECTION_ADMIN)
-from . import portal, telegram, matrix_auth
+from .handler import (
+ SECTION_ADMIN,
+ SECTION_AUTH,
+ SECTION_CREATING_PORTALS,
+ SECTION_MISC,
+ SECTION_PORTAL_MANAGEMENT,
+ CommandEvent,
+ CommandHandler,
+ CommandProcessor,
+ command_handler,
+)
-__all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent",
- "SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS",
- "SECTION_PORTAL_MANAGEMENT"]
+# This has to happen after the handler imports
+from . import matrix_auth, portal, telegram # isort: skip
+
+__all__ = [
+ "command_handler",
+ "CommandHandler",
+ "CommandProcessor",
+ "CommandEvent",
+ "SECTION_AUTH",
+ "SECTION_MISC",
+ "SECTION_ADMIN",
+ "SECTION_CREATING_PORTALS",
+ "SECTION_PORTAL_MANAGEMENT",
+]
diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py
index 0f09a98c..34e1227d 100644
--- a/mautrix_telegram/commands/handler.py
+++ b/mautrix_telegram/commands/handler.py
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
-# Copyright (C) 2019 Tulir Asokan
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -15,18 +15,22 @@
# along with this program. If not, see .
from __future__ import annotations
-from typing import Awaitable, Callable, NamedTuple, Any, TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, NamedTuple
from telethon.errors import FloodWaitError
-from mautrix.types import RoomID, EventID, MessageEventContent
-from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent,
- CommandHandler as BaseCommandHandler,
- CommandProcessor as BaseCommandProcessor,
- CommandHandlerFunc, command_handler as base_command_handler)
+from mautrix.bridge.commands import (
+ CommandEvent as BaseCommandEvent,
+ CommandHandler as BaseCommandHandler,
+ CommandHandlerFunc,
+ CommandProcessor as BaseCommandProcessor,
+ HelpSection,
+ command_handler as base_command_handler,
+)
+from mautrix.types import EventID, MessageEventContent, RoomID
from mautrix.util.format_duration import format_duration
-from .. import user as u, portal as po
+from .. import portal as po, user as u
if TYPE_CHECKING:
from ..__main__ import TelegramBridge
@@ -52,11 +56,31 @@ class CommandEvent(BaseCommandEvent):
sender: u.User
portal: po.Portal
- def __init__(self, processor: CommandProcessor, room_id: RoomID, event_id: EventID,
- sender: u.User, command: str, args: list[str], content: MessageEventContent,
- portal: po.Portal | None, is_management: bool, has_bridge_bot: bool) -> None:
- super().__init__(processor, room_id, event_id, sender, command, args, content,
- portal, is_management, has_bridge_bot)
+ def __init__(
+ self,
+ processor: CommandProcessor,
+ room_id: RoomID,
+ event_id: EventID,
+ sender: u.User,
+ command: str,
+ args: list[str],
+ content: MessageEventContent,
+ portal: po.Portal | None,
+ is_management: bool,
+ has_bridge_bot: bool,
+ ) -> None:
+ super().__init__(
+ processor,
+ room_id,
+ event_id,
+ sender,
+ command,
+ args,
+ content,
+ portal,
+ is_management,
+ has_bridge_bot,
+ )
self.bridge = processor.bridge
self.tgbot = processor.tgbot
self.config = processor.config
@@ -67,9 +91,14 @@ class CommandEvent(BaseCommandEvent):
return self.sender.is_admin
async def get_help_key(self) -> HelpCacheKey:
- return HelpCacheKey(self.is_management, self.portal is not None,
- self.sender.puppet_whitelisted, self.sender.matrix_puppet_whitelisted,
- self.sender.is_admin, await self.sender.is_logged_in())
+ return HelpCacheKey(
+ self.is_management,
+ self.portal is not None,
+ self.sender.puppet_whitelisted,
+ self.sender.matrix_puppet_whitelisted,
+ self.sender.is_admin,
+ await self.sender.is_logged_in(),
+ )
class CommandHandler(BaseCommandHandler):
@@ -78,14 +107,33 @@ class CommandHandler(BaseCommandHandler):
needs_puppeting: bool
needs_matrix_puppeting: bool
- def __init__(self, handler: Callable[[CommandEvent], Awaitable[EventID]],
- management_only: bool, name: str, help_text: str, help_args: str,
- help_section: HelpSection, needs_auth: bool, needs_puppeting: bool,
- needs_matrix_puppeting: bool, needs_admin: bool, **kwargs) -> None:
- super().__init__(handler, management_only, name, help_text, help_args, help_section,
- needs_auth=needs_auth, needs_puppeting=needs_puppeting,
- needs_matrix_puppeting=needs_matrix_puppeting, needs_admin=needs_admin,
- **kwargs)
+ def __init__(
+ self,
+ handler: Callable[[CommandEvent], Awaitable[EventID]],
+ management_only: bool,
+ name: str,
+ help_text: str,
+ help_args: str,
+ help_section: HelpSection,
+ needs_auth: bool,
+ needs_puppeting: bool,
+ needs_matrix_puppeting: bool,
+ needs_admin: bool,
+ **kwargs,
+ ) -> None:
+ super().__init__(
+ handler,
+ management_only,
+ name,
+ help_text,
+ help_args,
+ help_section,
+ needs_auth=needs_auth,
+ needs_puppeting=needs_puppeting,
+ needs_matrix_puppeting=needs_matrix_puppeting,
+ needs_admin=needs_admin,
+ **kwargs,
+ )
async def get_permission_error(self, evt: CommandEvent) -> str | None:
if self.needs_puppeting and not evt.sender.puppet_whitelisted:
@@ -95,33 +143,51 @@ class CommandHandler(BaseCommandHandler):
return await super().get_permission_error(evt)
def has_permission(self, key: HelpCacheKey) -> bool:
- return (super().has_permission(key) and
- (not self.needs_puppeting or key.puppet_whitelisted) and
- (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted))
+ return (
+ super().has_permission(key)
+ and (not self.needs_puppeting or key.puppet_whitelisted)
+ and (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted)
+ )
-def command_handler(_func: CommandHandlerFunc | None = None, *, needs_auth: bool = True,
- needs_puppeting: bool = True, needs_matrix_puppeting: bool = False,
- needs_admin: bool = False, management_only: bool = False,
- name: str | None = None, help_text: str = "", help_args: str = "",
- help_section: HelpSection = None) -> Callable[[CommandHandlerFunc],
- CommandHandler]:
+def command_handler(
+ _func: CommandHandlerFunc | None = None,
+ *,
+ needs_auth: bool = True,
+ needs_puppeting: bool = True,
+ needs_matrix_puppeting: bool = False,
+ needs_admin: bool = False,
+ management_only: bool = False,
+ name: str | None = None,
+ help_text: str = "",
+ help_args: str = "",
+ help_section: HelpSection = None,
+) -> Callable[[CommandHandlerFunc], CommandHandler]:
return base_command_handler(
- _func, _handler_class=CommandHandler, name=name, help_text=help_text, help_args=help_args,
- help_section=help_section, management_only=management_only, needs_auth=needs_auth,
- needs_admin=needs_admin, needs_puppeting=needs_puppeting,
- needs_matrix_puppeting=needs_matrix_puppeting)
+ _func,
+ _handler_class=CommandHandler,
+ name=name,
+ help_text=help_text,
+ help_args=help_args,
+ help_section=help_section,
+ management_only=management_only,
+ needs_auth=needs_auth,
+ needs_admin=needs_admin,
+ needs_puppeting=needs_puppeting,
+ needs_matrix_puppeting=needs_matrix_puppeting,
+ )
class CommandProcessor(BaseCommandProcessor):
- def __init__(self, bridge: 'TelegramBridge') -> None:
+ def __init__(self, bridge: "TelegramBridge") -> None:
super().__init__(event_class=CommandEvent, bridge=bridge)
self.tgbot = bridge.bot
self.public_website = bridge.public_website
@staticmethod
- async def _run_handler(handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent
- ) -> Any:
+ async def _run_handler(
+ handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent
+ ) -> Any:
try:
return await handler(evt)
except FloodWaitError as e:
diff --git a/mautrix_telegram/commands/matrix_auth.py b/mautrix_telegram/commands/matrix_auth.py
index cc85d693..8636d541 100644
--- a/mautrix_telegram/commands/matrix_auth.py
+++ b/mautrix_telegram/commands/matrix_auth.py
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
-# Copyright (C) 2019 Tulir Asokan
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,33 +13,27 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from mautrix.types import EventID
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
+from mautrix.types import EventID
-from . import command_handler, CommandEvent, SECTION_AUTH
from .. import puppet as pu
+from . import SECTION_AUTH, CommandEvent, command_handler
-@command_handler(needs_auth=True, needs_matrix_puppeting=True,
- help_section=SECTION_AUTH, help_text="Revert your Telegram account's Matrix "
- "puppet to use the default Matrix account.")
-async def logout_matrix(evt: CommandEvent) -> EventID:
- puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
- if not puppet.is_real_user:
- return await evt.reply("You are not logged in with your Matrix account.")
- await puppet.switch_mxid(None, None)
- return await evt.reply("Reverted your Telegram account's Matrix puppet back to the default.")
-
-
-@command_handler(needs_auth=True, management_only=True, needs_matrix_puppeting=True,
- help_section=SECTION_AUTH,
- help_text="Replace your Telegram account's Matrix puppet with your own Matrix "
- "account.")
+@command_handler(
+ needs_auth=True,
+ management_only=True,
+ needs_matrix_puppeting=True,
+ help_section=SECTION_AUTH,
+ help_text="Replace your Telegram account's Matrix puppet with your own Matrix account.",
+)
async def login_matrix(evt: CommandEvent) -> EventID:
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
if puppet.is_real_user:
- return await evt.reply("You have already logged in with your Matrix account. "
- "Log out with `$cmdprefix+sp logout-matrix` first.")
+ return await evt.reply(
+ "You have already logged in with your Matrix account. "
+ "Log out with `$cmdprefix+sp logout-matrix` first."
+ )
allow_matrix_login = evt.config.get("bridge.allow_matrix_login", True)
if allow_matrix_login:
evt.sender.command_status = {
@@ -57,57 +51,35 @@ async def login_matrix(evt: CommandEvent) -> EventID:
"here.\n"
f"If you would like to log in outside of Matrix, [click here]({url}).\n\n"
"Logging in outside of Matrix is recommended, because in-Matrix login would save "
- "your access token in the message history.")
- return await evt.reply("This bridge instance does not allow logging in inside Matrix.\n\n"
- f"Please visit [the login page]({url}) to log in.")
+ "your access token in the message history."
+ )
+ return await evt.reply(
+ "This bridge instance does not allow logging in inside Matrix.\n\n"
+ f"Please visit [the login page]({url}) to log in."
+ )
elif allow_matrix_login:
return await evt.reply(
"This bridge instance does not allow you to log in outside of Matrix.\n\n"
- "Please send your Matrix access token here to log in.")
+ "Please send your Matrix access token here to log in."
+ )
return await evt.reply("This bridge instance has been configured to not allow logging in.")
-@command_handler(needs_auth=True, needs_matrix_puppeting=True,
- help_section=SECTION_AUTH,
- help_text="Pings the server with the stored matrix authentication.")
-async def ping_matrix(evt: CommandEvent) -> EventID:
- puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
- if not puppet.is_real_user:
- return await evt.reply("You are not logged in with your Matrix account.")
- try:
- await puppet.start()
- except InvalidAccessToken:
- return await evt.reply("Your access token is invalid.")
- return await evt.reply("Your Matrix login is working.")
-
-
-@command_handler(needs_auth=True, needs_matrix_puppeting=True, help_section=SECTION_AUTH,
- help_text="Clear the Matrix sync token stored for your custom puppet.")
-async def clear_cache_matrix(evt: CommandEvent) -> EventID:
- puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
- if not puppet.is_real_user:
- return await evt.reply("You are not logged in with your Matrix account.")
- try:
- puppet.stop()
- puppet.next_batch = None
- await puppet.start()
- except InvalidAccessToken:
- return await evt.reply("Your access token is invalid.")
- return await evt.reply("Cleared cache successfully.")
-
-
async def enter_matrix_token(evt: CommandEvent) -> EventID:
evt.sender.command_status = None
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
if puppet.is_real_user:
- return await evt.reply("You have already logged in with your Matrix account. "
- "Log out with `$cmdprefix+sp logout-matrix` first.")
+ return await evt.reply(
+ "You have already logged in with your Matrix account. "
+ "Log out with `$cmdprefix+sp logout-matrix` first."
+ )
try:
await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
except OnlyLoginSelf:
return await evt.reply("You can only log in as your own Matrix user.")
except InvalidAccessToken:
return await evt.reply("Failed to verify access token.")
- return await evt.reply("Replaced your Telegram account's Matrix puppet "
- f"with {puppet.custom_mxid}.")
+ return await evt.reply(
+ "Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}."
+ )
diff --git a/mautrix_telegram/commands/portal/admin.py b/mautrix_telegram/commands/portal/admin.py
index 2bff444f..7b74f079 100644
--- a/mautrix_telegram/commands/portal/admin.py
+++ b/mautrix_telegram/commands/portal/admin.py
@@ -18,13 +18,16 @@ import asyncio
from mautrix.types import EventID
from ... import portal as po, puppet as pu, user as u
-from .. import command_handler, CommandEvent, SECTION_ADMIN
+from .. import SECTION_ADMIN, CommandEvent, command_handler
-@command_handler(needs_admin=True, needs_auth=False,
- help_section=SECTION_ADMIN,
- help_args="<`portal`|`puppet`|`user`>",
- help_text="Clear internal bridge caches")
+@command_handler(
+ needs_admin=True,
+ needs_auth=False,
+ help_section=SECTION_ADMIN,
+ help_args="<`portal`|`puppet`|`user`>",
+ help_text="Clear internal bridge caches",
+)
async def clear_db_cache(evt: CommandEvent) -> EventID:
try:
section = evt.args[0].lower()
@@ -44,19 +47,19 @@ async def clear_db_cache(evt: CommandEvent) -> EventID:
)
await evt.reply("Cleared puppet cache and restarted custom puppet syncers")
elif section == "user":
- u.User.by_mxid = {
- user.mxid: user
- for user in u.User.by_tgid.values()
- }
+ u.User.by_mxid = {user.mxid: user for user in u.User.by_tgid.values()}
await evt.reply("Cleared non-logged-in user cache")
else:
return await evt.reply("**Usage:** `$cmdprefix+sp clear-db-cache `")
-@command_handler(needs_admin=True, needs_auth=False,
- help_section=SECTION_ADMIN,
- help_args="[_mxid_]",
- help_text="Reload and reconnect a user")
+@command_handler(
+ needs_admin=True,
+ needs_auth=False,
+ help_section=SECTION_ADMIN,
+ help_args="[_mxid_]",
+ help_text="Reload and reconnect a user",
+)
async def reload_user(evt: CommandEvent) -> EventID:
if len(evt.args) > 0:
mxid = evt.args[0]
diff --git a/mautrix_telegram/commands/portal/bridge.py b/mautrix_telegram/commands/portal/bridge.py
index 6b67ff22..40b9fed0 100644
--- a/mautrix_telegram/commands/portal/bridge.py
+++ b/mautrix_telegram/commands/portal/bridge.py
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
-# Copyright (C) 2019 Tulir Asokan
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -18,26 +18,31 @@ from __future__ import annotations
from typing import Awaitable
import asyncio
-from telethon.tl.types import ChatForbidden, ChannelForbidden
+from telethon.tl.types import ChannelForbidden, ChatForbidden
from mautrix.types import EventID, RoomID
-from ...types import TelegramID
from ... import portal as po
-from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
-from .util import user_has_power_level, get_initial_state, warn_missing_power
+from ...types import TelegramID
+from .. import SECTION_CREATING_PORTALS, CommandEvent, command_handler
+from .util import get_initial_state, user_has_power_level, warn_missing_power
-@command_handler(needs_auth=False, needs_puppeting=False,
- help_section=SECTION_CREATING_PORTALS,
- help_args="[_id_]",
- help_text="Bridge the current Matrix room to the Telegram chat with the given "
- "ID. The ID must be the prefixed version that you get with the `/id` "
- "command of the Telegram-side bot.")
+@command_handler(
+ needs_auth=False,
+ needs_puppeting=False,
+ help_section=SECTION_CREATING_PORTALS,
+ help_args="[_id_]",
+ help_text=(
+ "Bridge the current Matrix room to the Telegram chat with the given ID. The ID must be "
+ "the prefixed version that you get with the `/id` command of the Telegram-side bot."
+ ),
+)
async def bridge(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
- return await evt.reply("**Usage:** "
- "`$cmdprefix+sp bridge [Matrix room ID]`")
+ return await evt.reply(
+ "**Usage:** `$cmdprefix+sp bridge [Matrix room ID]`"
+ )
force_use_bot = False
if evt.args[0] == "--usebot" and evt.sender.is_admin:
force_use_bot = True
@@ -61,24 +66,30 @@ async def bridge(evt: CommandEvent) -> EventID:
tgid = TelegramID(-int(tgid_str))
peer_type = "chat"
else:
- return await evt.reply("That doesn't seem like a prefixed Telegram chat ID.\n\n"
- "If you did not get the ID using the `/id` bot command, please "
- "prefix channel IDs with `-100` and normal group IDs with `-`.\n\n"
- "Bridging private chats to existing rooms is not allowed.")
+ return await evt.reply(
+ "That doesn't seem like a prefixed Telegram chat ID.\n\n"
+ "If you did not get the ID using the `/id` bot command, please "
+ "prefix channel IDs with `-100` and normal group IDs with `-`.\n\n"
+ "Bridging private chats to existing rooms is not allowed."
+ )
portal = await po.Portal.get_by_tgid(tgid, peer_type=peer_type)
if not portal.allow_bridging:
- return await evt.reply("This bridge doesn't allow bridging that Telegram chat.\n"
- "If you're the bridge admin, try "
- "`$cmdprefix+sp filter whitelist ` first.")
+ return await evt.reply(
+ "This bridge doesn't allow bridging that Telegram chat.\n"
+ "If you're the bridge admin, try "
+ "`$cmdprefix+sp filter whitelist ` first."
+ )
if portal.mxid:
has_portal_message = (
"That Telegram chat already has a portal at "
- f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). ")
+ f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). "
+ )
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, "unbridge"):
- return await evt.reply(f"{has_portal_message}"
- "Additionally, you do not have the permissions to unbridge "
- "that room.")
+ return await evt.reply(
+ f"{has_portal_message}"
+ "Additionally, you do not have the permissions to unbridge that room."
+ )
evt.sender.command_status = {
"next": confirm_bridge,
"action": "Room bridging",
@@ -88,12 +99,14 @@ async def bridge(evt: CommandEvent) -> EventID:
"peer_type": portal.peer_type,
"force_use_bot": force_use_bot,
}
- return await evt.reply(f"{has_portal_message}"
- "However, you have the permissions to unbridge that room.\n\n"
- "To delete that portal completely and continue bridging, use "
- "`$cmdprefix+sp delete-and-continue`. To unbridge the portal "
- "without kicking Matrix users, use `$cmdprefix+sp unbridge-and-"
- "continue`. To cancel, use `$cmdprefix+sp cancel`")
+ return await evt.reply(
+ f"{has_portal_message}"
+ "However, you have the permissions to unbridge that room.\n\n"
+ "To delete that portal completely and continue bridging, use "
+ "`$cmdprefix+sp delete-and-continue`. To unbridge the portal "
+ "without kicking Matrix users, use `$cmdprefix+sp unbridge-and-"
+ "continue`. To cancel, use `$cmdprefix+sp cancel`"
+ )
evt.sender.command_status = {
"next": confirm_bridge,
"action": "Room bridging",
@@ -102,29 +115,36 @@ async def bridge(evt: CommandEvent) -> EventID:
"peer_type": portal.peer_type,
"force_use_bot": force_use_bot,
}
- return await evt.reply("That Telegram chat has no existing portal. To confirm bridging the "
- "chat to this room, use `$cmdprefix+sp continue`")
+ return await evt.reply(
+ "That Telegram chat has no existing portal. To confirm bridging the "
+ "chat to this room, use `$cmdprefix+sp continue`"
+ )
-async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Portal"
- ) -> tuple[bool, Awaitable[None] | None]:
+async def cleanup_old_portal_while_bridging(
+ evt: CommandEvent, portal: po.Portal
+) -> tuple[bool, Awaitable[None] | None]:
if not portal.mxid:
- await evt.reply("The portal seems to have lost its Matrix room between you"
- "calling `$cmdprefix+sp bridge` and this command.\n\n"
- "Continuing without touching previous Matrix room...")
+ await evt.reply(
+ "The portal seems to have lost its Matrix room between you"
+ "calling `$cmdprefix+sp bridge` and this command.\n\n"
+ "Continuing without touching previous Matrix room..."
+ )
return True, None
elif evt.args[0] == "delete-and-continue":
return True, portal.cleanup_portal("Portal deleted (moving to another room)", delete=False)
elif evt.args[0] == "unbridge-and-continue":
- return True, portal.cleanup_portal("Room unbridged (portal moving to another room)",
- puppets_only=True, delete=False)
+ return True, portal.cleanup_portal(
+ "Room unbridged (portal moving to another room)", puppets_only=True, delete=False
+ )
else:
await evt.reply(
"The chat you were trying to bridge already has a Matrix portal room.\n\n"
"Please use `$cmdprefix+sp delete-and-continue` or `$cmdprefix+sp unbridge-and-"
"continue` to either delete or unbridge the existing room (respectively) and "
"continue with the bridging.\n\n"
- "If you changed your mind, use `$cmdprefix+sp cancel` to cancel.")
+ "If you changed your mind, use `$cmdprefix+sp cancel` to cancel."
+ )
return False, None
@@ -135,9 +155,10 @@ async def confirm_bridge(evt: CommandEvent) -> EventID | None:
bridge_to_mxid = status["bridge_to_mxid"]
except KeyError:
evt.sender.command_status = None
- return await evt.reply("Fatal error: tgid or peer_type missing from command_status. "
- "This shouldn't happen unless you're messing with the command "
- "handler code.")
+ return await evt.reply(
+ "Fatal error: tgid or peer_type missing from command_status. "
+ "This shouldn't happen unless you're messing with the command handler code."
+ )
is_logged_in = await evt.sender.is_logged_in() and not status["force_use_bot"]
@@ -150,32 +171,41 @@ async def confirm_bridge(evt: CommandEvent) -> EventID | None:
await evt.reply("Cleaning up previous portal room...")
elif portal.mxid:
evt.sender.command_status = None
- return await evt.reply("The portal seems to have created a Matrix room between you "
- "calling `$cmdprefix+sp bridge` and this command.\n\n"
- "Please start over by calling the bridge command again.")
+ return await evt.reply(
+ "The portal seems to have created a Matrix room between you "
+ "calling `$cmdprefix+sp bridge` and this command.\n\n"
+ "Please start over by calling the bridge command again."
+ )
elif evt.args[0] != "continue":
- return await evt.reply("Please use `$cmdprefix+sp continue` to confirm the bridging or "
- "`$cmdprefix+sp cancel` to cancel.")
+ return await evt.reply(
+ "Please use `$cmdprefix+sp continue` to confirm the bridging or "
+ "`$cmdprefix+sp cancel` to cancel."
+ )
evt.sender.command_status = None
async with portal._room_create_lock:
- await _locked_confirm_bridge(evt, portal=portal, room_id=bridge_to_mxid,
- is_logged_in=is_logged_in)
+ await _locked_confirm_bridge(
+ evt, portal=portal, room_id=bridge_to_mxid, is_logged_in=is_logged_in
+ )
-async def _locked_confirm_bridge(evt: CommandEvent, portal: 'po.Portal', room_id: RoomID,
- is_logged_in: bool) -> EventID | None:
+async def _locked_confirm_bridge(
+ evt: CommandEvent, portal: po.Portal, room_id: RoomID, is_logged_in: bool
+) -> EventID | None:
user = evt.sender if is_logged_in else evt.tgbot
try:
entity = await user.client.get_entity(portal.peer)
except Exception:
evt.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer)
if is_logged_in:
- return await evt.reply("Failed to get info of telegram chat. "
- "You are logged in, are you in that chat?")
+ return await evt.reply(
+ "Failed to get info of telegram chat. You are logged in, are you in that chat?"
+ )
else:
- return await evt.reply("Failed to get info of telegram chat. "
- "You're not logged in, is the relay bot in the chat?")
+ return await evt.reply(
+ "Failed to get info of telegram chat. "
+ "You're not logged in, is the relay bot in the chat?"
+ )
if isinstance(entity, (ChatForbidden, ChannelForbidden)):
if is_logged_in:
return await evt.reply("You don't seem to be in that chat.")
@@ -184,14 +214,14 @@ async def _locked_confirm_bridge(evt: CommandEvent, portal: 'po.Portal', room_id
portal.mxid = room_id
portal.by_mxid[portal.mxid] = portal
- (portal.title, portal.about, levels,
- portal.encrypted) = await get_initial_state(evt.az.intent, evt.room_id)
+ (portal.title, portal.about, levels, portal.encrypted) = await get_initial_state(
+ evt.az.intent, evt.room_id
+ )
portal.photo_id = ""
await portal.save()
await portal.update_bridge_info()
- asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
- loop=evt.loop)
+ asyncio.create_task(portal.update_matrix_room(user, entity, direct=False, levels=levels))
await warn_missing_power(levels, evt)
diff --git a/mautrix_telegram/commands/portal/config.py b/mautrix_telegram/commands/portal/config.py
index 5a867c7f..7a01c3ac 100644
--- a/mautrix_telegram/commands/portal/config.py
+++ b/mautrix_telegram/commands/portal/config.py
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
-# Copyright (C) 2019 Tulir Asokan
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,21 +13,26 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Awaitable, Any
+from __future__ import annotations
+
+from typing import Any, Awaitable
from io import StringIO
from ruamel.yaml import YAMLError
-from mautrix.util.config import yaml
from mautrix.types import EventID
+from mautrix.util.config import yaml
from ... import portal as po, util
-from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
+from .. import SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler
-@command_handler(needs_auth=False, help_section=SECTION_PORTAL_MANAGEMENT,
- help_text="View or change per-portal settings.",
- help_args="<`help`|_subcommand_> [...]")
+@command_handler(
+ needs_auth=False,
+ help_section=SECTION_PORTAL_MANAGEMENT,
+ help_text="View or change per-portal settings.",
+ help_args="<`help`|_subcommand_> [...]",
+)
async def config(evt: CommandEvent) -> None:
cmd = evt.args[0].lower() if len(evt.args) > 0 else "help"
if cmd not in ("view", "defaults", "set", "unset", "add", "del"):
@@ -67,7 +72,8 @@ async def config(evt: CommandEvent) -> None:
def config_help(evt: CommandEvent) -> Awaitable[EventID]:
- return evt.reply("""**Usage:** `$cmdprefix config [...]`. Subcommands:
+ return evt.reply(
+ """**Usage:** `$cmdprefix config [...]`. Subcommands:
* **help** - View this help text.
* **view** - View the current config data.
@@ -76,7 +82,8 @@ def config_help(evt: CommandEvent) -> Awaitable[EventID]:
* **unset** <_key_> - Remove a config value.
* **add** <_key_> <_value_> - Add a value to an array.
* **del** <_key_> <_value_> - Remove a value from an array.
-""")
+"""
+ )
def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[EventID]:
@@ -84,18 +91,20 @@ def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[EventID]:
def config_defaults(evt: CommandEvent) -> Awaitable[EventID]:
- value = _str_value({
- "bridge_notices": {
- "default": evt.config["bridge.bridge_notices.default"],
- "exceptions": evt.config["bridge.bridge_notices.exceptions"],
- },
- "bot_messages_as_notices": evt.config["bridge.bot_messages_as_notices"],
- "inline_images": evt.config["bridge.inline_images"],
- "message_formats": evt.config["bridge.message_formats"],
- "emote_format": evt.config["bridge.emote_format"],
- "state_event_formats": evt.config["bridge.state_event_formats"],
- "telegram_link_preview": evt.config["bridge.telegram_link_preview"],
- })
+ value = _str_value(
+ {
+ "bridge_notices": {
+ "default": evt.config["bridge.bridge_notices.default"],
+ "exceptions": evt.config["bridge.bridge_notices.exceptions"],
+ },
+ "bot_messages_as_notices": evt.config["bridge.bot_messages_as_notices"],
+ "inline_images": evt.config["bridge.inline_images"],
+ "message_formats": evt.config["bridge.message_formats"],
+ "emote_format": evt.config["bridge.emote_format"],
+ "state_event_formats": evt.config["bridge.state_event_formats"],
+ "telegram_link_preview": evt.config["bridge.telegram_link_preview"],
+ }
+ )
return evt.reply(f"Bridge instance wide config:\n{value.rstrip()}")
@@ -115,8 +124,7 @@ def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: Any) -> Aw
elif util.recursive_set(portal.local_config, key, value):
return evt.reply(f"Successfully set the value of `{key}` to {_str_value(value)}".rstrip())
else:
- return evt.reply(f"Failed to set value of `{key}`. "
- "Does the path contain non-map types?")
+ return evt.reply(f"Failed to set value of `{key}`. Does the path contain non-map types?")
def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[EventID]:
@@ -128,15 +136,17 @@ def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[Ev
return evt.reply(f"`{key}` not found in config.")
-def config_add_del(evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str
- ) -> Awaitable[EventID]:
+def config_add_del(
+ evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str
+) -> Awaitable[EventID]:
if not key or value is None:
return evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} `")
arr = util.recursive_get(portal.local_config, key)
if not arr:
- return evt.reply(f"`{key}` not found in config. "
- f"Maybe do `$cmdprefix+sp config set {key} []` first?")
+ return evt.reply(
+ f"`{key}` not found in config. Maybe do `$cmdprefix+sp config set {key} []` first?"
+ )
elif not isinstance(arr, list):
return evt.reply("`{key}` does not seem to be an array.")
elif cmd == "add":
diff --git a/mautrix_telegram/commands/portal/create_chat.py b/mautrix_telegram/commands/portal/create_chat.py
index 7c304a68..707fab3f 100644
--- a/mautrix_telegram/commands/portal/create_chat.py
+++ b/mautrix_telegram/commands/portal/create_chat.py
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
-# Copyright (C) 2019 Tulir Asokan
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,24 +13,30 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from __future__ import annotations
+
from mautrix.types import EventID
from ... import portal as po
from ...types import TelegramID
-from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
-from .util import user_has_power_level, get_initial_state, warn_missing_power
+from .. import SECTION_CREATING_PORTALS, CommandEvent, command_handler
+from .util import get_initial_state, user_has_power_level, warn_missing_power
-@command_handler(help_section=SECTION_CREATING_PORTALS,
- help_args="[_type_]",
- help_text="Create a Telegram chat of the given type for the current Matrix room. "
- "The type is either `group`, `supergroup` or `channel` (defaults to "
- "`supergroup`).")
+@command_handler(
+ help_section=SECTION_CREATING_PORTALS,
+ help_args="[_type_]",
+ help_text=(
+ "Create a Telegram chat of the given type for the current Matrix room. "
+ "The type is either `group`, `supergroup` or `channel` (defaults to `supergroup`)."
+ ),
+)
async def create(evt: CommandEvent) -> EventID:
type = evt.args[0] if len(evt.args) > 0 else "supergroup"
if type not in ("chat", "group", "supergroup", "channel"):
return await evt.reply(
- "**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`")
+ "**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`"
+ )
if await po.Portal.get_by_mxid(evt.room_id):
return await evt.reply("This is already a portal room.")
@@ -50,14 +56,23 @@ async def create(evt: CommandEvent) -> EventID:
"group": "chat",
}[type]
- portal = po.Portal(tgid=TelegramID(0), tg_receiver=TelegramID(0), peer_type=type,
- mxid=evt.room_id, title=title, about=about, encrypted=encrypted)
+ portal = po.Portal(
+ tgid=TelegramID(0),
+ tg_receiver=TelegramID(0),
+ peer_type=type,
+ mxid=evt.room_id,
+ title=title,
+ about=about,
+ encrypted=encrypted,
+ )
invites, errors = await portal.get_telegram_users_in_matrix_room(evt.sender)
if len(errors) > 0:
error_list = "\n".join(f"* [{mxid}](https://matrix.to/#/{mxid})" for mxid in errors)
- await evt.reply(f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
- "You can try `$cmdprefix+sp search -r ` to help the bridge find "
- "those users.")
+ await evt.reply(
+ f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
+ "You can try `$cmdprefix+sp search -r ` to help the bridge find "
+ "those users."
+ )
await warn_missing_power(levels, evt)
diff --git a/mautrix_telegram/commands/portal/filter.py b/mautrix_telegram/commands/portal/filter.py
index d02b12e4..318829cd 100644
--- a/mautrix_telegram/commands/portal/filter.py
+++ b/mautrix_telegram/commands/portal/filter.py
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
-# Copyright (C) 2019 Tulir Asokan
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,17 +13,20 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from __future__ import annotations
+
from mautrix.types import EventID
from ... import portal as po
-from .. import command_handler, CommandEvent, SECTION_ADMIN
+from .. import SECTION_ADMIN, CommandEvent, command_handler
-@command_handler(needs_admin=True,
- help_section=SECTION_ADMIN,
- help_args="<`whitelist`|`blacklist`>",
- help_text="Change whether the bridge will allow or disallow bridging rooms by "
- "default.")
+@command_handler(
+ needs_admin=True,
+ help_section=SECTION_ADMIN,
+ help_args="<`whitelist`|`blacklist`>",
+ help_text="Change whether the bridge will allow or disallow bridging rooms by default.",
+)
async def filter_mode(evt: CommandEvent) -> EventID:
try:
mode = evt.args[0]
@@ -36,19 +39,26 @@ async def filter_mode(evt: CommandEvent) -> EventID:
evt.config.save()
po.Portal.filter_mode = mode
if mode == "whitelist":
- return await evt.reply("The bridge will now disallow bridging chats by default.\n"
- "To allow bridging a specific chat, use"
- "`!filter whitelist `.")
+ return await evt.reply(
+ "The bridge will now disallow bridging chats by default.\n"
+ "To allow bridging a specific chat, use"
+ "`!filter whitelist `."
+ )
else:
- return await evt.reply("The bridge will now allow bridging chats by default.\n"
- "To disallow bridging a specific chat, use"
- "`!filter blacklist `.")
+ return await evt.reply(
+ "The bridge will now allow bridging chats by default.\n"
+ "To disallow bridging a specific chat, use"
+ "`!filter blacklist `."
+ )
-@command_handler(name="filter", needs_admin=True,
- help_section=SECTION_ADMIN,
- help_args="<`whitelist`|`blacklist`> <_chat ID_>",
- help_text="Allow or disallow bridging a specific chat.")
+@command_handler(
+ name="filter",
+ needs_admin=True,
+ help_section=SECTION_ADMIN,
+ help_args="<`whitelist`|`blacklist`> <_chat ID_>",
+ help_text="Allow or disallow bridging a specific chat.",
+)
async def edit_filter(evt: CommandEvent) -> EventID:
try:
action = evt.args[0]
@@ -67,7 +77,7 @@ async def edit_filter(evt: CommandEvent) -> EventID:
mode = evt.config["bridge.filter.mode"]
if mode not in ("blacklist", "whitelist"):
- return await evt.reply(f"Unknown filter mode \"{mode}\". Please fix the bridge config.")
+ return await evt.reply(f'Unknown filter mode "{mode}". Please fix the bridge config.')
filter_id_list = evt.config["bridge.filter.list"]
diff --git a/mautrix_telegram/commands/portal/misc.py b/mautrix_telegram/commands/portal/misc.py
index cd4dddba..970334ad 100644
--- a/mautrix_telegram/commands/portal/misc.py
+++ b/mautrix_telegram/commands/portal/misc.py
@@ -15,24 +15,33 @@
# along with this program. If not, see .
from __future__ import annotations
-from datetime import timedelta, datetime
+from datetime import datetime, timedelta
import re
+from telethon.errors import (
+ ChatAdminRequiredError,
+ RPCError,
+ UsernameInvalidError,
+ UsernameNotModifiedError,
+ UsernameOccupiedError,
+)
from telethon.tl.functions.channels import GetFullChannelRequest
from telethon.tl.functions.messages import GetFullChatRequest
-from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
- UsernameNotModifiedError, UsernameOccupiedError, RPCError)
from mautrix.types import EventID
from ... import portal as po
-from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT, SECTION_MISC
+from .. import SECTION_MISC, SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler
from .util import user_has_power_level
-@command_handler(needs_admin=False, needs_puppeting=False, needs_auth=False,
- help_section=SECTION_MISC,
- help_text="Fetch Matrix room state to ensure the bridge has up-to-date info.")
+@command_handler(
+ needs_admin=False,
+ needs_puppeting=False,
+ needs_auth=False,
+ help_section=SECTION_MISC,
+ help_text="Fetch Matrix room state to ensure the bridge has up-to-date info.",
+)
async def sync_state(evt: CommandEvent) -> EventID:
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
@@ -44,8 +53,9 @@ async def sync_state(evt: CommandEvent) -> EventID:
await evt.reply("Synchronization complete")
-@command_handler(needs_admin=False, needs_puppeting=False, needs_auth=False,
- help_section=SECTION_MISC)
+@command_handler(
+ needs_admin=False, needs_puppeting=False, needs_auth=False, help_section=SECTION_MISC
+)
async def sync_full(evt: CommandEvent) -> EventID:
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
@@ -70,9 +80,14 @@ async def sync_full(evt: CommandEvent) -> EventID:
return await evt.reply("Portal synced successfully.")
-@command_handler(name="id", needs_admin=False, needs_puppeting=False, needs_auth=False,
- help_section=SECTION_MISC,
- help_text="Get the ID of the Telegram chat where this room is bridged.")
+@command_handler(
+ name="id",
+ needs_admin=False,
+ needs_puppeting=False,
+ needs_auth=False,
+ help_section=SECTION_MISC,
+ help_text="Get the ID of the Telegram chat where this room is bridged.",
+)
async def get_id(evt: CommandEvent) -> EventID:
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
@@ -85,12 +100,14 @@ async def get_id(evt: CommandEvent) -> EventID:
await evt.reply(f"This room is bridged to Telegram chat ID `{tgid}`.")
-invite_link_usage = ("**Usage:** `$cmdprefix+sp invite-link [--uses=] [--expire=]`"
- "\n\n"
- "* `--uses`: the number of times the invite link can be used."
- " Defaults to unlimited.\n"
- "* `--expire`: the duration after which the link will expire."
- " A number suffixed with d(ay), h(our), m(inute) or s(econd)")
+invite_link_usage = (
+ "**Usage:** `$cmdprefix+sp invite-link [--uses=] [--expire=]`"
+ "\n\n"
+ "* `--uses`: the number of times the invite link can be used."
+ " Defaults to unlimited.\n"
+ "* `--expire`: the duration after which the link will expire."
+ " A number suffixed with d(ay), h(our), m(inute) or s(econd)"
+)
def _parse_flag(args: list[str]) -> tuple[str, str]:
@@ -99,7 +116,7 @@ def _parse_flag(args: list[str]) -> tuple[str, str]:
value_start = arg.index("=")
if value_start:
flag = arg[2:value_start]
- value = arg[value_start+1:]
+ value = arg[value_start + 1 :]
else:
flag = arg[2:]
value = args.pop(0).lower()
@@ -114,7 +131,9 @@ def _parse_flag(args: list[str]) -> tuple[str, str]:
return flag, value
-delta_regex = re.compile("([0-9]+)(w(?:eek)?|d(?:ay)?|h(?:our)?|m(?:in(?:ute)?)?|s(?:ec(?:ond)?)?)")
+delta_regex = re.compile(
+ "([0-9]+)(w(?:eek)?|d(?:ay)?|h(?:our)?|m(?:in(?:ute)?)?|s(?:ec(?:ond)?)?)"
+)
def _parse_delta(value: str) -> timedelta | None:
@@ -137,9 +156,11 @@ def _parse_delta(value: str) -> timedelta | None:
return None
-@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
- help_text="Get a Telegram invite link to the current chat.",
- help_args="[--uses=] [--expire=