From 6d25e9687ea89a613751efa05b7cc27f93d58894 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 21 Dec 2021 01:36:24 +0200 Subject: [PATCH] Blacken and isort code --- .github/workflows/python-lint.yml | 18 + .isort.cfg | 16 - README.md | 2 + mautrix_telegram/__main__.py | 11 +- mautrix_telegram/abstract_user.py | 223 ++- mautrix_telegram/bot.py | 88 +- mautrix_telegram/commands/__init__.py | 32 +- mautrix_telegram/commands/handler.py | 146 +- mautrix_telegram/commands/matrix_auth.py | 86 +- mautrix_telegram/commands/portal/admin.py | 29 +- mautrix_telegram/commands/portal/bridge.py | 150 +- mautrix_telegram/commands/portal/config.py | 64 +- .../commands/portal/create_chat.py | 43 +- mautrix_telegram/commands/portal/filter.py | 46 +- mautrix_telegram/commands/portal/misc.py | 90 +- mautrix_telegram/commands/portal/unbridge.py | 74 +- mautrix_telegram/commands/portal/util.py | 20 +- mautrix_telegram/commands/telegram/account.py | 110 +- mautrix_telegram/commands/telegram/auth.py | 251 ++- mautrix_telegram/commands/telegram/misc.py | 207 ++- mautrix_telegram/config.py | 44 +- mautrix_telegram/db/__init__.py | 18 +- mautrix_telegram/db/bot_chat.py | 2 +- mautrix_telegram/db/message.py | 13 +- mautrix_telegram/db/portal.py | 21 +- mautrix_telegram/db/puppet.py | 24 +- mautrix_telegram/db/telegram_file.py | 18 +- mautrix_telegram/db/telethon_session.py | 17 +- .../db/upgrade/v01_initial_revision.py | 26 +- mautrix_telegram/db/user.py | 19 +- .../formatter/from_matrix/__init__.py | 8 +- .../formatter/from_matrix/parser.py | 13 +- .../formatter/from_matrix/telegram_message.py | 43 +- mautrix_telegram/formatter/from_telegram.py | 147 +- mautrix_telegram/get_version.py | 8 +- mautrix_telegram/matrix.py | 204 ++- mautrix_telegram/portal.py | 1503 +++++++++++------ mautrix_telegram/puppet.py | 86 +- mautrix_telegram/tgclient.py | 53 +- mautrix_telegram/types.py | 2 +- mautrix_telegram/user.py | 212 ++- mautrix_telegram/util/__init__.py | 10 +- mautrix_telegram/util/color_log.py | 18 +- mautrix_telegram/util/deduplication.py | 62 +- mautrix_telegram/util/file_transfer.py | 217 ++- mautrix_telegram/util/media_fallback.py | 15 +- .../util/parallel_file_transfer.py | 234 ++- mautrix_telegram/util/recursive_dict.py | 12 +- mautrix_telegram/util/send_lock.py | 8 +- mautrix_telegram/util/tgs_converter.py | 145 +- mautrix_telegram/version.py | 2 +- mautrix_telegram/web/common/auth_api.py | 294 +++- mautrix_telegram/web/provisioning/__init__.py | 398 +++-- mautrix_telegram/web/public/__init__.py | 156 +- pyproject.toml | 12 + 55 files changed, 3752 insertions(+), 2018 deletions(-) create mode 100644 .github/workflows/python-lint.yml delete mode 100644 .isort.cfg create mode 100644 pyproject.toml 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://img.shields.io/github/license/mautrix/telegram.svg)](LICENSE) [![Release](https://img.shields.io/github/release/mautrix/telegram/all.svg)](https://github.com/mautrix/telegram/releases) [![GitLab CI](https://mau.dev/mautrix/telegram/badges/master/pipeline.svg)](https://mau.dev/mautrix/telegram/container_registry) +[![Code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](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=