diff --git a/example-config.yaml b/example-config.yaml index 4a19329f..5586aa8e 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -73,6 +73,11 @@ metrics: enabled: false listen_port: 8000 +# Manhole config. +manhole: + enabled: false + path: /var/tmp/mautrix-telegram.manhole + # Bridge config bridge: # Localpart template of MXIDs for Telegram users. diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 35714151..1a7e07d0 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -13,8 +13,8 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional from itertools import chain -import sys from alchemysession import AlchemySessionContainer @@ -23,6 +23,7 @@ from mautrix.bridge.db import Base from .web.provisioning import ProvisioningAPI from .web.public import PublicBridgeWebsite +from .commands.manhole import ManholeState from .abstract_user import init as init_abstract_user from .bot import Bot, init as init_bot from .config import Config @@ -55,6 +56,7 @@ class TelegramBridge(Bridge): config: Config session_container: AlchemySessionContainer bot: Bot + manhole: Optional[ManholeState] def prepare_db(self) -> None: super().prepare_db() @@ -84,9 +86,10 @@ class TelegramBridge(Bridge): def prepare_bridge(self) -> None: self.bot = init_bot(self.config) - context = Context(self.az, self.config, self.loop, self.session_container, self.bot) + context = Context(self.az, self.config, self.loop, self.session_container, self, self.bot) self._prepare_website(context) self.matrix = context.mx = MatrixHandler(context) + self.manhole = None init_abstract_user(context) init_formatter(context) @@ -100,6 +103,9 @@ class TelegramBridge(Bridge): for puppet in Puppet.by_custom_mxid.values(): puppet.stop() self.shutdown_actions = (user.stop() for user in User.by_tgid.values()) + if self.manhole: + self.manhole.close() + self.manhole = None TelegramBridge().run() diff --git a/mautrix_telegram/commands/__init__.py b/mautrix_telegram/commands/__init__.py index b33fe63b..cbdd778c 100644 --- a/mautrix_telegram/commands/__init__.py +++ b/mautrix_telegram/commands/__init__.py @@ -1,7 +1,7 @@ from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent, SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT, SECTION_MISC, SECTION_ADMIN) -from . import portal, telegram, clean_rooms, matrix_auth +from . import portal, telegram, clean_rooms, matrix_auth, manhole __all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent", "SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS", diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 63a6f17b..f50e59e5 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -46,6 +46,7 @@ class CommandEvent(BaseCommandEvent): is_portal: bool) -> None: super().__init__(processor, room_id, event_id, sender, command, args, is_management, is_portal) + self.bridge = processor.bridge self.tgbot = processor.tgbot self.config = processor.config self.public_website = processor.public_website @@ -113,6 +114,7 @@ class CommandProcessor(BaseCommandProcessor): super().__init__(az=context.az, config=context.config, event_class=CommandEvent, loop=context.loop) self.tgbot = context.bot + self.bridge = context.bridge self.az, self.config, self.loop, self.tgbot = context.core self.public_website = context.public_website self.command_prefix = self.config["bridge.command_prefix"] diff --git a/mautrix_telegram/commands/manhole.py b/mautrix_telegram/commands/manhole.py new file mode 100644 index 00000000..6b560da6 --- /dev/null +++ b/mautrix_telegram/commands/manhole.py @@ -0,0 +1,95 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2019 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Optional, Callable +import asyncio +import sys +import os + +from attr import dataclass + +from telethon import __version__ as __telethon_version__ + +from mautrix import __version__ as __mautrix_version__ +from mautrix.types import UserID +from mautrix.errors import MatrixConnectionError +from mautrix.util.manhole import start_manhole + +from .. import __version__ +from . import command_handler, CommandEvent, SECTION_ADMIN + + +@dataclass +class ManholeState: + server: Optional[asyncio.AbstractServer] = None + opened_by: Optional[UserID] = None + close: Optional[Callable[[], None]] = None + + +@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN, + help_text="Open a manhole into the bridge.") +async def manhole(evt: CommandEvent) -> None: + if not evt.config["manhole.enabled"]: + await evt.reply("The manhole has been disabled in the config.") + return + + if evt.bridge.manhole: + await evt.reply(f"There's an existing manhole opened by {evt.bridge.manhole.opened_by}") + return + + from ..portal import Portal + from ..puppet import Puppet + from ..user import User + namespace = { + "bridge": evt.bridge, + "User": User, + "Portal": Portal, + "Puppet": Puppet, + } + banner = (f"Python {sys.version} on {sys.platform}\n" + f"mautrix-telegram {__version__} with mautrix-python {__mautrix_version__} " + f"and Telethon {__telethon_version__}\n\nManhole opened by {evt.sender.mxid}\n") + path = evt.config["manhole.path"] + + evt.log.info(f"{evt.sender.mxid} opened a manhole.") + server, close = await start_manhole(path=path, banner=banner, namespace=namespace, + loop=evt.loop) + evt.bridge.manhole = ManholeState(server=server, opened_by=evt.sender.mxid, close=close) + await evt.reply(f"Opened manhole at unix://{path}") + await server.wait_closed() + evt.bridge.manhole = None + try: + os.unlink(path) + except FileNotFoundError: + pass + evt.log.info(f"{evt.sender.mxid}'s manhole was closed.") + try: + await evt.reply("Your manhole was closed.") + except (AttributeError, MatrixConnectionError) as e: + evt.log.warning(f"Failed to send manhole close notification: {e}") + + +@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN, + help_text="Close an open manhole.") +async def close_manhole(evt: CommandEvent) -> None: + if not evt.bridge.manhole: + await evt.reply("There is no open manhole.") + return + + opened_by = evt.bridge.manhole.opened_by + evt.bridge.manhole.close() + evt.bridge.manhole = None + if opened_by != evt.sender.mxid: + await evt.reply(f"Closed manhole opened by {opened_by}") diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 65240713..c59a6e1c 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -74,6 +74,9 @@ class Config(BaseBridgeConfig): copy("metrics.enabled") copy("metrics.listen_port") + copy("manhole.enabled") + copy("manhole.path") + copy("bridge.username_template") copy("bridge.alias_template") copy("bridge.displayname_template") diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py index 4566de3f..3ed15e4f 100644 --- a/mautrix_telegram/context.py +++ b/mautrix_telegram/context.py @@ -25,12 +25,14 @@ if TYPE_CHECKING: from .config import Config from .bot import Bot from .matrix import MatrixHandler + from .__main__ import TelegramBridge class Context: az: AppService config: 'Config' loop: asyncio.AbstractEventLoop + bridge: 'TelegramBridge' bot: Optional['Bot'] mx: Optional['MatrixHandler'] session_container: AlchemySessionContainer @@ -38,10 +40,12 @@ class Context: provisioning_api: Optional['ProvisioningAPI'] def __init__(self, az: AppService, config: 'Config', loop: asyncio.AbstractEventLoop, - session_container: AlchemySessionContainer, bot: Optional['Bot']) -> None: + session_container: AlchemySessionContainer, bridge: 'TelegramBridge', + bot: Optional['Bot']) -> None: self.az = az self.config = config self.loop = loop + self.bridge = bridge self.bot = bot self.mx = None self.session_container = session_container diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index c181d9bf..3d3ab7fa 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -27,6 +27,7 @@ from telethon.tl.functions.account import UpdateStatusRequest from mautrix.client import Client from mautrix.errors import MatrixRequestError from mautrix.types import UserID +from mautrix.bridge import BaseUser from .types import TelegramID from .db import User as DBUser @@ -42,7 +43,7 @@ config: Optional['Config'] = None SearchResult = NewType('SearchResult', Tuple['pu.Puppet', int]) -class User(AbstractUser): +class User(AbstractUser, BaseUser): log: logging.Logger = logging.getLogger("mau.user") by_mxid: Dict[str, 'User'] = {} by_tgid: Dict[int, 'User'] = {} diff --git a/setup.py b/setup.py index a8e824a8..8fb78995 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setuptools.setup( install_requires=[ "aiohttp>=3.0.1,<4", - "mautrix>=0.4.0.dev53,<0.5", + "mautrix>=0.4.0.dev54,<0.5", "SQLAlchemy>=1.2.3,<2", "alembic>=1.0.0,<2", "commonmark>=0.8.1,<1",