From f6923a5e1b518ac5754ef825cec65dede9cb4dbf Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 24 Jun 2018 21:22:12 +0300 Subject: [PATCH 01/24] Add provisioning API config (ref #154) --- example-config.yaml | 11 +++++++++++ mautrix_telegram/__main__.py | 16 +++++++++++----- mautrix_telegram/abstract_user.py | 2 +- mautrix_telegram/config.py | 4 ++++ mautrix_telegram/context.py | 4 ++-- mautrix_telegram/provisioning_api.py | 27 +++++++++++++++++++++++++++ 6 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 mautrix_telegram/provisioning_api.py diff --git a/example-config.yaml b/example-config.yaml index 27c64991..6f9e8df5 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -33,6 +33,17 @@ appservice: # implicitly. external: https://example.com/public + # Provisioning API part of the web server for automated portal creation and fetching information. + # Used by things like Dimension (https://dimension.t2bot.io/). + provisioning: + # Whether or not the provisioning API should be enabled. + enabled: true + # The prefix to use in the provisioning API endpoints. + prefix: /_matrix/provision + # The shared secret to authorize users of the API. + # You can generate a decent secret with `pwgen -snc 32 1` + shared_secret: "Very secret shared secret" + # The unique ID of this appservice. id: telegram # Username of the appservice bot. diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 8fce6699..84d01698 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -39,6 +39,7 @@ from .portal import init as init_portal from .puppet import init as init_puppet from .formatter import init as init_formatter from .public import PublicBridgeWebsite +from .provisioning_api import ProvisioningAPI from .context import Context parser = argparse.ArgumentParser( @@ -74,9 +75,9 @@ db_factory = orm.sessionmaker(bind=db_engine) db_session = orm.scoping.scoped_session(db_factory) Base.metadata.bind = db_engine -telethon_session_container = AlchemySessionContainer(engine=db_engine, session=db_session, - table_base=Base, table_prefix="telethon_", - manage_tables=False) +session_container = AlchemySessionContainer(engine=db_engine, session=db_session, + table_base=Base, table_prefix="telethon_", + manage_tables=False) loop = asyncio.get_event_loop() @@ -85,11 +86,16 @@ appserv = AppService(config["homeserver.address"], config["homeserver.domain"], config["appservice.bot_username"], log="mau.as", loop=loop, verify_ssl=config["homeserver.verify_ssl"]) -context = Context(appserv, db_session, config, loop, None, None, telethon_session_container) +context = Context(appserv, db_session, config, loop, None, None, session_container) if config["appservice.public.enabled"]: public = PublicBridgeWebsite(loop) - appserv.app.add_subapp(config.get("appservice.public.prefix", "/public"), public.app) + appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public.app) + +if config["appservice.provisioning.enabled"]: + provisioning_api = ProvisioningAPI(loop) + appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning", + provisioning_api.app) with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start: init_db(db_session) diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index dd1fe03b..d15e9e66 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -328,5 +328,5 @@ class AbstractUser: def init(context): global config, MAX_DELETIONS AbstractUser.az, AbstractUser.db, config, AbstractUser.loop, _ = context - AbstractUser.session_container = context.telethon_session_container + AbstractUser.session_container = context.session_container MAX_DELETIONS = config.get("bridge.max_telegram_delete", 10) diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index fb2e14a2..07944b3c 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -159,6 +159,10 @@ class Config(DictWithRecursion): copy("appservice.public.prefix") copy("appservice.public.external") + copy("appservice.provisioning.enabled") + copy("appservice.provisioning.prefix") + copy("appservice.provisioning.shared_secret") + copy("appservice.id") copy("appservice.bot_username") copy("appservice.bot_displayname") diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py index 530f1eed..dfb32b59 100644 --- a/mautrix_telegram/context.py +++ b/mautrix_telegram/context.py @@ -17,14 +17,14 @@ class Context: - def __init__(self, az, db, config, loop, bot, mx, telethon_session_container): + def __init__(self, az, db, config, loop, bot, mx, session_container): self.az = az self.db = db self.config = config self.loop = loop self.bot = bot self.mx = mx - self.telethon_session_container = telethon_session_container + self.session_container = session_container def __iter__(self): yield self.az diff --git a/mautrix_telegram/provisioning_api.py b/mautrix_telegram/provisioning_api.py new file mode 100644 index 00000000..db89c506 --- /dev/null +++ b/mautrix_telegram/provisioning_api.py @@ -0,0 +1,27 @@ +# -*- coding: future_fstrings -*- +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 aiohttp import web +import logging + + +class ProvisioningAPI: + log = logging.getLogger("mau.provisioning") + + def __init__(self, loop): + self.loop = loop + + self.app = web.Application(loop=loop) From 5d48040eb8bda89b41c7f304a1903df1541f9eba Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 24 Jun 2018 21:35:07 +0300 Subject: [PATCH 02/24] Separate auth methods from public API --- mautrix_telegram/public/__init__.py | 125 ++++++---------------------- mautrix_telegram/public/auth_api.py | 115 +++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 99 deletions(-) create mode 100644 mautrix_telegram/public/auth_api.py diff --git a/mautrix_telegram/public/__init__.py b/mautrix_telegram/public/__init__.py index 6a463f2e..60e22fd6 100644 --- a/mautrix_telegram/public/__init__.py +++ b/mautrix_telegram/public/__init__.py @@ -26,12 +26,14 @@ from ..user import User from ..commands.auth import enter_password from ..util import format_duration +from .auth_api import AuthAPI -class PublicBridgeWebsite: + +class PublicBridgeWebsite(AuthAPI): log = logging.getLogger("mau.public") def __init__(self, loop): - self.loop = loop + super(AuthAPI, self).__init__(loop) self.login = Template( pkg_resources.resource_string("mautrix_telegram", "public/login.html.mako")) @@ -43,22 +45,24 @@ class PublicBridgeWebsite: pkg_resources.resource_filename("mautrix_telegram", "public/")) async def get_login(self, request): - user = (User.get_by_mxid(request.rel_url.query["mxid"], create=False) - if "mxid" in request.rel_url.query else None) state = "token" if request.rel_url.query.get("mode", "") == "bot" else "request" + + mxid = request.rel_url.query.get("mxid", None) + user = User.get_by_mxid(mxid, create=False) if mxid else None + if not user: - return self.render_login( - mxid=request.rel_url.query["mxid"] if "mxid" in request.rel_url.query else None, - state=state) + return self.get_login_response(mxid=mxid, state=state) elif not user.puppet_whitelisted: - return self.render_login(mxid=user.mxid, error="You are not whitelisted.", status=403) + return self.get_login_response(mxid=user.mxid, error="You are not whitelisted.", + status=403) await user.ensure_started() if not await user.is_logged_in(): - return self.render_login(mxid=user.mxid, state=state) + return self.get_login_response(mxid=user.mxid, state=state) - return self.render_login(mxid=user.mxid, username=user.username) + return self.get_login_response(mxid=user.mxid, username=user.username) - def render_login(self, status=200, username="", state="", error="", message="", mxid=""): + def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", + errcode=""): return web.Response(status=status, content_type="text/html", text=self.login.render(username=username, state=state, error=error, message=message, mxid=mxid)) @@ -69,101 +73,24 @@ class PublicBridgeWebsite: asyncio.ensure_future(user.post_login(user_info), loop=self.loop) if user.command_status and user.command_status["action"] == "Login": user.command_status = None - return self.render_login(mxid=user.mxid, state="logged-in", status=200, - username=user_info.username) + return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, + username=user_info.username) except Exception: self.log.exception("Error sending bot token") - return self.render_login(mxid=user.mxid, state="token", status=500, - error="Internal server error while sending token.") - - async def post_login_phone(self, user, phone): - try: - await user.client.sign_in(phone or "+123") - return self.render_login(mxid=user.mxid, state="code", status=200, - message="Code requested successfully.") - except PhoneNumberInvalidError: - return self.render_login(mxid=user.mxid, state="request", status=400, - error="Invalid phone number.") - except PhoneNumberUnoccupiedError: - return self.render_login(mxid=user.mxid, state="request", status=404, - error="That phone number has not been registered.") - except PhoneNumberFloodError: - return self.render_login( - mxid=user.mxid, state="request", status=429, - error="Your phone number has been temporarily blocked for flooding. " - "The ban is usually applied for around a day.") - except FloodWaitError as e: - return self.render_login( - mxid=user.mxid, state="request", status=429, - error="Your phone number has been temporarily blocked for flooding. " - f"Please wait for {format_duration(e.seconds)} before trying again.") - except PhoneNumberBannedError: - return self.render_login(mxid=user.mxid, state="request", status=401, - error="Your phone number is banned from Telegram.") - except PhoneNumberAppSignupForbiddenError: - return self.render_login(mxid=user.mxid, state="request", status=401, - error="You have disabled 3rd party apps on your account.") - except Exception: - self.log.exception("Error requesting phone code") - return self.render_login(mxid=user.mxid, state="request", status=500, - error="Internal server error while requesting code.") - - async def post_login_code(self, user, code, password_in_data): - try: - user_info = await user.client.sign_in(code=code) - asyncio.ensure_future(user.post_login(user_info), loop=self.loop) - if user.command_status and user.command_status["action"] == "Login": - user.command_status = None - return self.render_login(mxid=user.mxid, state="logged-in", status=200, - username=user_info.username) - except PhoneCodeInvalidError: - return self.render_login(mxid=user.mxid, state="code", status=403, - error="Incorrect phone code.") - except PhoneCodeExpiredError: - return self.render_login(mxid=user.mxid, state="code", status=403, - error="Phone code expired.") - except SessionPasswordNeededError: - if not password_in_data: - if user.command_status and user.command_status["action"] == "Login": - user.command_status = { - "next": enter_password, - "action": "Login (password entry)", - } - return self.render_login( - mxid=user.mxid, state="password", status=200, - message="Code accepted, but you have 2-factor authentication is enabled.") - return None - except Exception: - self.log.exception("Error sending phone code") - return self.render_login(mxid=user.mxid, state="code", status=500, - error="Internal server error while sending code.") - - async def post_login_password(self, user, password): - try: - user_info = await user.client.sign_in(password=password) - asyncio.ensure_future(user.post_login(user_info), loop=self.loop) - if user.command_status and user.command_status["action"] == "Login (password entry)": - user.command_status = None - return self.render_login(mxid=user.mxid, state="logged-in", status=200, - username=user_info.username) - except (PasswordHashInvalidError, PasswordEmptyError): - return self.render_login(mxid=user.mxid, state="password", status=400, - error="Incorrect password.") - except Exception: - self.log.exception("Error sending password") - return self.render_login(mxid=user.mxid, state="password", status=500, - error="Internal server error while sending password.") + return self.get_login_response(mxid=user.mxid, state="token", status=500, + error="Internal server error while sending token.") async def post_login(self, request): data = await request.post() if "mxid" not in data: - return self.render_login(error="Please enter your Matrix ID.", status=400) + return self.get_login_response(error="Please enter your Matrix ID.", status=400) - user = await User.get_by_mxid(data["mxid"]).ensure_started() + user = await User.get_by_mxid(data["mxid"]).ensure_started(even_if_no_session=True) if not user.puppet_whitelisted: - return self.render_login(mxid=user.mxid, error="You are not whitelisted.", status=403) + return self.get_login_response(mxid=user.mxid, error="You are not whitelisted.", + status=403) elif await user.is_logged_in(): - return self.render_login(mxid=user.mxid, username=user.username) + return self.get_login_response(mxid=user.mxid, username=user.username) await user.ensure_started(even_if_no_session=True) @@ -177,8 +104,8 @@ class PublicBridgeWebsite: if resp or "password" not in data: return resp elif "password" not in data: - return self.render_login(error="No data given.", status=400) + return self.get_login_response(error="No data given.", status=400) if "password" in data: return await self.post_login_password(user, data["password"]) - return self.render_login(error="This should never happen.", status=500) + return self.get_login_response(error="This should never happen.", status=500) diff --git a/mautrix_telegram/public/auth_api.py b/mautrix_telegram/public/auth_api.py new file mode 100644 index 00000000..ec763452 --- /dev/null +++ b/mautrix_telegram/public/auth_api.py @@ -0,0 +1,115 @@ +# -*- coding: future_fstrings -*- +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 abc import abstractmethod +import abc +import asyncio +import logging + +from telethon.errors import * + +from ..commands.auth import enter_password +from ..util import format_duration + + +class AuthAPI(abc.ABC): + log = logging.getLogger("mau.public.auth") + + def __init__(self, loop): + self.loop = loop + + @abstractmethod + def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", + errcode=""): + raise NotImplementedError() + + async def post_login_phone(self, user, phone): + try: + await user.client.sign_in(phone or "+123") + return self.get_login_response(mxid=user.mxid, state="code", status=200, + message="Code requested successfully.") + except PhoneNumberInvalidError: + return self.get_login_response(mxid=user.mxid, state="request", status=400, + error="Invalid phone number.") + except PhoneNumberUnoccupiedError: + return self.get_login_response(mxid=user.mxid, state="request", status=404, + error="That phone number has not been registered.") + except PhoneNumberFloodError: + return self.get_login_response( + mxid=user.mxid, state="request", status=429, + error="Your phone number has been temporarily blocked for flooding. " + "The ban is usually applied for around a day.") + except FloodWaitError as e: + return self.get_login_response( + mxid=user.mxid, state="request", status=429, + error="Your phone number has been temporarily blocked for flooding. " + f"Please wait for {format_duration(e.seconds)} before trying again.") + except PhoneNumberBannedError: + return self.get_login_response(mxid=user.mxid, state="request", status=401, + error="Your phone number is banned from Telegram.") + except PhoneNumberAppSignupForbiddenError: + return self.get_login_response(mxid=user.mxid, state="request", status=401, + error="You have disabled 3rd party apps on your account.") + except Exception: + self.log.exception("Error requesting phone code") + return self.get_login_response(mxid=user.mxid, state="request", status=500, + error="Internal server error while requesting code.") + + async def post_login_code(self, user, code, password_in_data): + try: + user_info = await user.client.sign_in(code=code) + asyncio.ensure_future(user.post_login(user_info), loop=self.loop) + if user.command_status and user.command_status["action"] == "Login": + user.command_status = None + return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, + username=user_info.username) + except PhoneCodeInvalidError: + return self.get_login_response(mxid=user.mxid, state="code", status=403, + error="Incorrect phone code.") + except PhoneCodeExpiredError: + return self.get_login_response(mxid=user.mxid, state="code", status=403, + error="Phone code expired.") + except SessionPasswordNeededError: + if not password_in_data: + if user.command_status and user.command_status["action"] == "Login": + user.command_status = { + "next": enter_password, + "action": "Login (password entry)", + } + return self.get_login_response( + mxid=user.mxid, state="password", status=200, + message="Code accepted, but you have 2-factor authentication is enabled.") + return None + except Exception: + self.log.exception("Error sending phone code") + return self.get_login_response(mxid=user.mxid, state="code", status=500, + error="Internal server error while sending code.") + + async def post_login_password(self, user, password): + try: + user_info = await user.client.sign_in(password=password) + asyncio.ensure_future(user.post_login(user_info), loop=self.loop) + if user.command_status and user.command_status["action"] == "Login (password entry)": + user.command_status = None + return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, + username=user_info.username) + except (PasswordHashInvalidError, PasswordEmptyError): + return self.get_login_response(mxid=user.mxid, state="password", status=400, + error="Incorrect password.") + except Exception: + self.log.exception("Error sending password") + return self.get_login_response(mxid=user.mxid, state="password", status=500, + error="Internal server error while sending password.") From fa30cb5c1f6cdff7329be426701ef5ed3966b25b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 24 Jun 2018 21:39:01 +0300 Subject: [PATCH 03/24] Move web stuff to web package --- mautrix_telegram/__main__.py | 4 +-- mautrix_telegram/web/__init__.py | 2 ++ mautrix_telegram/web/common/__init__.py | 1 + .../{public => web/common}/auth_api.py | 32 +++++++++--------- .../provisioning/__init__.py} | 0 mautrix_telegram/web/provisioning/spec.yaml | 0 mautrix_telegram/{ => web}/public/__init__.py | 9 ++--- mautrix_telegram/{ => web}/public/favicon.png | Bin mautrix_telegram/{ => web}/public/login.css | 0 .../{ => web}/public/login.html.mako | 0 10 files changed, 23 insertions(+), 25 deletions(-) create mode 100644 mautrix_telegram/web/__init__.py create mode 100644 mautrix_telegram/web/common/__init__.py rename mautrix_telegram/{public => web/common}/auth_api.py (79%) rename mautrix_telegram/{provisioning_api.py => web/provisioning/__init__.py} (100%) create mode 100644 mautrix_telegram/web/provisioning/spec.yaml rename mautrix_telegram/{ => web}/public/__init__.py (96%) rename mautrix_telegram/{ => web}/public/favicon.png (100%) rename mautrix_telegram/{ => web}/public/login.css (100%) rename mautrix_telegram/{ => web}/public/login.html.mako (100%) diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 84d01698..9702468f 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -38,8 +38,8 @@ from .bot import init as init_bot from .portal import init as init_portal from .puppet import init as init_puppet from .formatter import init as init_formatter -from .public import PublicBridgeWebsite -from .provisioning_api import ProvisioningAPI +from .web.public import PublicBridgeWebsite +from .web.provisioning import ProvisioningAPI from .context import Context parser = argparse.ArgumentParser( diff --git a/mautrix_telegram/web/__init__.py b/mautrix_telegram/web/__init__.py new file mode 100644 index 00000000..002510e8 --- /dev/null +++ b/mautrix_telegram/web/__init__.py @@ -0,0 +1,2 @@ +from .provisioning import ProvisioningAPI +from .public import PublicBridgeWebsite diff --git a/mautrix_telegram/web/common/__init__.py b/mautrix_telegram/web/common/__init__.py new file mode 100644 index 00000000..ccb0d922 --- /dev/null +++ b/mautrix_telegram/web/common/__init__.py @@ -0,0 +1 @@ +from .auth_api import AuthAPI diff --git a/mautrix_telegram/public/auth_api.py b/mautrix_telegram/web/common/auth_api.py similarity index 79% rename from mautrix_telegram/public/auth_api.py rename to mautrix_telegram/web/common/auth_api.py index ec763452..9502db53 100644 --- a/mautrix_telegram/public/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -21,8 +21,8 @@ import logging from telethon.errors import * -from ..commands.auth import enter_password -from ..util import format_duration +from mautrix_telegram.commands.auth import enter_password +from mautrix_telegram.util import format_duration class AuthAPI(abc.ABC): @@ -33,20 +33,20 @@ class AuthAPI(abc.ABC): @abstractmethod def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", - errcode=""): + errcode=""): raise NotImplementedError() async def post_login_phone(self, user, phone): try: await user.client.sign_in(phone or "+123") return self.get_login_response(mxid=user.mxid, state="code", status=200, - message="Code requested successfully.") + message="Code requested successfully.") except PhoneNumberInvalidError: return self.get_login_response(mxid=user.mxid, state="request", status=400, - error="Invalid phone number.") + error="Invalid phone number.") except PhoneNumberUnoccupiedError: return self.get_login_response(mxid=user.mxid, state="request", status=404, - error="That phone number has not been registered.") + error="That phone number has not been registered.") except PhoneNumberFloodError: return self.get_login_response( mxid=user.mxid, state="request", status=429, @@ -59,14 +59,14 @@ class AuthAPI(abc.ABC): f"Please wait for {format_duration(e.seconds)} before trying again.") except PhoneNumberBannedError: return self.get_login_response(mxid=user.mxid, state="request", status=401, - error="Your phone number is banned from Telegram.") + error="Your phone number is banned from Telegram.") except PhoneNumberAppSignupForbiddenError: return self.get_login_response(mxid=user.mxid, state="request", status=401, - error="You have disabled 3rd party apps on your account.") + error="You have disabled 3rd party apps on your account.") except Exception: self.log.exception("Error requesting phone code") return self.get_login_response(mxid=user.mxid, state="request", status=500, - error="Internal server error while requesting code.") + error="Internal server error while requesting code.") async def post_login_code(self, user, code, password_in_data): try: @@ -75,13 +75,13 @@ class AuthAPI(abc.ABC): if user.command_status and user.command_status["action"] == "Login": user.command_status = None return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, - username=user_info.username) + username=user_info.username) except PhoneCodeInvalidError: return self.get_login_response(mxid=user.mxid, state="code", status=403, - error="Incorrect phone code.") + error="Incorrect phone code.") except PhoneCodeExpiredError: return self.get_login_response(mxid=user.mxid, state="code", status=403, - error="Phone code expired.") + error="Phone code expired.") except SessionPasswordNeededError: if not password_in_data: if user.command_status and user.command_status["action"] == "Login": @@ -96,7 +96,7 @@ class AuthAPI(abc.ABC): except Exception: self.log.exception("Error sending phone code") return self.get_login_response(mxid=user.mxid, state="code", status=500, - error="Internal server error while sending code.") + error="Internal server error while sending code.") async def post_login_password(self, user, password): try: @@ -105,11 +105,11 @@ class AuthAPI(abc.ABC): if user.command_status and user.command_status["action"] == "Login (password entry)": user.command_status = None return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, - username=user_info.username) + username=user_info.username) except (PasswordHashInvalidError, PasswordEmptyError): return self.get_login_response(mxid=user.mxid, state="password", status=400, - error="Incorrect password.") + error="Incorrect password.") except Exception: self.log.exception("Error sending password") return self.get_login_response(mxid=user.mxid, state="password", status=500, - error="Internal server error while sending password.") + error="Internal server error while sending password.") diff --git a/mautrix_telegram/provisioning_api.py b/mautrix_telegram/web/provisioning/__init__.py similarity index 100% rename from mautrix_telegram/provisioning_api.py rename to mautrix_telegram/web/provisioning/__init__.py diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml new file mode 100644 index 00000000..e69de29b diff --git a/mautrix_telegram/public/__init__.py b/mautrix_telegram/web/public/__init__.py similarity index 96% rename from mautrix_telegram/public/__init__.py rename to mautrix_telegram/web/public/__init__.py index 60e22fd6..c08b1bc6 100644 --- a/mautrix_telegram/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -20,13 +20,8 @@ import asyncio import pkg_resources import logging -from telethon.errors import * - -from ..user import User -from ..commands.auth import enter_password -from ..util import format_duration - -from .auth_api import AuthAPI +from ...user import User +from ..common import AuthAPI class PublicBridgeWebsite(AuthAPI): diff --git a/mautrix_telegram/public/favicon.png b/mautrix_telegram/web/public/favicon.png similarity index 100% rename from mautrix_telegram/public/favicon.png rename to mautrix_telegram/web/public/favicon.png diff --git a/mautrix_telegram/public/login.css b/mautrix_telegram/web/public/login.css similarity index 100% rename from mautrix_telegram/public/login.css rename to mautrix_telegram/web/public/login.css diff --git a/mautrix_telegram/public/login.html.mako b/mautrix_telegram/web/public/login.html.mako similarity index 100% rename from mautrix_telegram/public/login.html.mako rename to mautrix_telegram/web/public/login.html.mako From f07009d0d2d509b22398d77a5f13a9633658faaa Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 24 Jun 2018 23:19:29 +0300 Subject: [PATCH 04/24] Add initial parts of provisioning API spec --- mautrix_telegram/web/provisioning/spec.yaml | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index e69de29b..7b0dc01f 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -0,0 +1,70 @@ +tags: + - + name: login + description: 'Authentication endpoints.' +definitions: + Error: + x-oad-type: object + type: object + title: Error + properties: + errcode: + x-oad-type: string + type: string + title: 'Error code' + description: 'A machine-readable error code' + error: + x-oad-type: string + type: string + title: Error + description: 'A human-readable description of the error' + status: + x-oad-type: integer + type: integer + title: Status + description: 'The HTTP status code' + format: int32 + AuthSuccess: + x-oad-type: object + type: object + properties: + state: + x-oad-type: string + type: string + enum: + - code + - request + - password + - token + - logged-in +security: + - + Bearer: [] +securityDefinitions: + Bearer: + description: 'Required authentication for all endpoints' + name: Authorization + in: header + type: apiKey +info: + title: 'mautrix-telegram provisioning' + version: 0.3.0 + description: 'The provisioning API for mautrix-telegram.' + contact: + name: 'Tulir Asokan' + email: tulir@maunium.net + url: 'https://maunium.net' + license: + name: AGPLv3 + url: 'https://github.com/tulir/mautrix-telegram/blob/master/LICENSE' +externalDocs: + description: 'Provisioning API wiki page on GitHub.' + url: 'https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API' +basePath: /_matrix/provisioning +schemes: + - https +consumes: + - application/json +produces: + - application/json +swagger: '2.0' From c0ceb1b2b007ad0f878a4c11ae993e7eb7f59b24 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 12 Jul 2018 23:45:15 +0300 Subject: [PATCH 05/24] Move post_login_token to common/auth_api --- mautrix_telegram/web/common/auth_api.py | 13 +++++++++++++ mautrix_telegram/web/public/__init__.py | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py index 9502db53..3ce557ba 100644 --- a/mautrix_telegram/web/common/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -68,6 +68,19 @@ class AuthAPI(abc.ABC): return self.get_login_response(mxid=user.mxid, state="request", status=500, error="Internal server error while requesting code.") + async def post_login_token(self, user, token): + try: + user_info = await user.client.sign_in(bot_token=token) + asyncio.ensure_future(user.post_login(user_info), loop=self.loop) + if user.command_status and user.command_status["action"] == "Login": + user.command_status = None + return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, + username=user_info.username) + except Exception: + self.log.exception("Error sending bot token") + return self.get_login_response(mxid=user.mxid, state="token", status=500, + error="Internal server error while sending token.") + async def post_login_code(self, user, code, password_in_data): try: user_info = await user.client.sign_in(code=code) diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py index c08b1bc6..89a76fd7 100644 --- a/mautrix_telegram/web/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -62,19 +62,6 @@ class PublicBridgeWebsite(AuthAPI): text=self.login.render(username=username, state=state, error=error, message=message, mxid=mxid)) - async def post_login_token(self, user, token): - try: - user_info = await user.client.sign_in(bot_token=token) - asyncio.ensure_future(user.post_login(user_info), loop=self.loop) - if user.command_status and user.command_status["action"] == "Login": - user.command_status = None - return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, - username=user_info.username) - except Exception: - self.log.exception("Error sending bot token") - return self.get_login_response(mxid=user.mxid, state="token", status=500, - error="Internal server error while sending token.") - async def post_login(self, request): data = await request.post() if "mxid" not in data: From 1fd920255fc15856d82b96c7322febfd6848e81b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 21:25:51 +0300 Subject: [PATCH 06/24] Finish initial provisioning API spec and impl --- mautrix_telegram/web/common/auth_api.py | 44 +- mautrix_telegram/web/provisioning/__init__.py | 68 ++- mautrix_telegram/web/provisioning/spec.yaml | 437 +++++++++++++++--- mautrix_telegram/web/public/__init__.py | 1 - 4 files changed, 480 insertions(+), 70 deletions(-) diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py index 3ce557ba..6980f1af 100644 --- a/mautrix_telegram/web/common/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -43,29 +43,34 @@ class AuthAPI(abc.ABC): message="Code requested successfully.") except PhoneNumberInvalidError: return self.get_login_response(mxid=user.mxid, state="request", status=400, + errcode="phone_number_invalid", error="Invalid phone number.") + except PhoneNumberBannedError: + return self.get_login_response(mxid=user.mxid, state="request", status=403, + errcode="phone_number_banned", + error="Your phone number is banned from Telegram.") + except PhoneNumberAppSignupForbiddenError: + return self.get_login_response(mxid=user.mxid, state="request", status=403, + errcode="phone_number_app_signup_forbidden", + error="You have disabled 3rd party apps on your account.") except PhoneNumberUnoccupiedError: return self.get_login_response(mxid=user.mxid, state="request", status=404, + errcode="phone_number_unoccupied", error="That phone number has not been registered.") except PhoneNumberFloodError: return self.get_login_response( - mxid=user.mxid, state="request", status=429, + mxid=user.mxid, state="request", status=429, errcode="phone_number_flood", error="Your phone number has been temporarily blocked for flooding. " "The ban is usually applied for around a day.") except FloodWaitError as e: return self.get_login_response( - mxid=user.mxid, state="request", status=429, + mxid=user.mxid, state="request", status=429, errcode="flood_wait", error="Your phone number has been temporarily blocked for flooding. " f"Please wait for {format_duration(e.seconds)} before trying again.") - except PhoneNumberBannedError: - return self.get_login_response(mxid=user.mxid, state="request", status=401, - error="Your phone number is banned from Telegram.") - except PhoneNumberAppSignupForbiddenError: - return self.get_login_response(mxid=user.mxid, state="request", status=401, - error="You have disabled 3rd party apps on your account.") except Exception: self.log.exception("Error requesting phone code") return self.get_login_response(mxid=user.mxid, state="request", status=500, + errcode="exception", error="Internal server error while requesting code.") async def post_login_token(self, user, token): @@ -76,6 +81,14 @@ class AuthAPI(abc.ABC): user.command_status = None return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, username=user_info.username) + except AccessTokenInvalidError: + return self.get_login_response(mxid=user.mxid, state="token", status=401, + errcode="bot_token_invalid", + error="Bot token invalid.") + except AccessTokenExpiredError: + return self.get_login_response(mxid=user.mxid, state="token", status=403, + errcode="bot_token_expired", + error="Bot token expired.") except Exception: self.log.exception("Error sending bot token") return self.get_login_response(mxid=user.mxid, state="token", status=500, @@ -90,10 +103,12 @@ class AuthAPI(abc.ABC): return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, username=user_info.username) except PhoneCodeInvalidError: - return self.get_login_response(mxid=user.mxid, state="code", status=403, + return self.get_login_response(mxid=user.mxid, state="code", status=401, + errcode="phone_code_invalid", error="Incorrect phone code.") except PhoneCodeExpiredError: return self.get_login_response(mxid=user.mxid, state="code", status=403, + errcode="phone_code_expired", error="Phone code expired.") except SessionPasswordNeededError: if not password_in_data: @@ -103,12 +118,13 @@ class AuthAPI(abc.ABC): "action": "Login (password entry)", } return self.get_login_response( - mxid=user.mxid, state="password", status=200, + mxid=user.mxid, state="password", status=202, message="Code accepted, but you have 2-factor authentication is enabled.") return None except Exception: self.log.exception("Error sending phone code") return self.get_login_response(mxid=user.mxid, state="code", status=500, + errcode="exception", error="Internal server error while sending code.") async def post_login_password(self, user, password): @@ -119,10 +135,16 @@ class AuthAPI(abc.ABC): user.command_status = None return self.get_login_response(mxid=user.mxid, state="logged-in", status=200, username=user_info.username) - except (PasswordHashInvalidError, PasswordEmptyError): + except PasswordEmptyError: return self.get_login_response(mxid=user.mxid, state="password", status=400, + errcode="password_empty", + error="Empty password.") + except PasswordHashInvalidError: + return self.get_login_response(mxid=user.mxid, state="password", status=401, + errcode="password_invalid", error="Incorrect password.") except Exception: self.log.exception("Error sending password") return self.get_login_response(mxid=user.mxid, state="password", status=500, + errcode="exception", error="Internal server error while sending password.") diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index db89c506..05c73816 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -17,11 +17,75 @@ from aiohttp import web import logging +from ..common import AuthAPI -class ProvisioningAPI: + +class ProvisioningAPI(AuthAPI): log = logging.getLogger("mau.provisioning") def __init__(self, loop): - self.loop = loop + super(AuthAPI, self).__init__(loop) self.app = web.Application(loop=loop) + + login_prefix = "/login/{mxid:@[^:]*:.+}" + self.app.router.add_route("POST", f"{login_prefix}/bot_token", self.send_bot_token) + self.app.router.add_route("POST", f"{login_prefix}/request_code", self.request_code) + self.app.router.add_route("POST", f"{login_prefix}/send_code", self.send_code) + self.app.router.add_route("POST", f"{login_prefix}/send_password", self.send_password) + + def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", + errcode=""): + if username: + resp = { + "state": "logged-in", + "username": username, + } + elif message: + resp = { + "message": message + } + else: + resp = { + "error": error, + "errcode": errcode, + } + return web.json_response(resp, status=status) + + async def get_user(self, request: web.Request): + mxid = request.match_info["mxid"] + user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) + if not user.puppet_whitelisted: + return user, self.get_login_response(mxid=user.mxid, error="You are not whitelisted.", + errcode="mxid_not_whitelisted", status=403) + elif await user.is_logged_in(): + return user, self.get_login_response(mxid=user.mxid, username=user.username, status=409) + return user, None + + async def send_bot_token(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_token(user, data.get("token", "")) + + async def request_code(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_phone(user, data.get("phone", "")) + + async def send_code(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_code(user, data.get("code", 0), password_in_data=False) + + async def send_password(self, request: web.Request): + user, err = await self.get_user(request) + if err: + return err + data = await request.json() + return await self.post_login_password(user, data.get("password", "")) diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index 7b0dc01f..e8fe08f3 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -1,70 +1,395 @@ +swagger: "2.0" + +info: + title: mautrix-telegram provisioning + version: 0.3.0 + description: The provisioning API for mautrix-telegram. + contact: + name: Tulir Asokan + email: tulir@maunium.net + url: https://maunium.net + license: + name: AGPLv3 + url: https://github.com/tulir/mautrix-telegram/blob/master/LICENSE + +externalDocs: + description: Provisioning API wiki page on GitHub. + url: https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API + +basePath: /_matrix/provision + +schemes: [https] +consumes: [application/json] +produces: [application/json] + tags: - - - name: login - description: 'Authentication endpoints.' +- name: Authentication + +paths: + /login/{mxid}/bot_token: + post: + operationId: post_bot_token + summary: Log in with a bot token + tags: [Authentication] + responses: + 200: + description: Login successful + schema: + $ref: "#/definitions/AuthSuccess" + 400: + $ref: "#/responses/MissingMXIDError" + 401: + description: Invalid or expired bot token + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: bot_token_ + enum: + - bot_token_invalid + - bot_token_expired + error: + $ref: "#/definitions/HumanReadableError" + 403: + $ref: "#/responses/NotWhitelistedError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + token: + type: string + description: The access token of the bot to log in as + example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv" + /login/{mxid}/request_code: + post: + operationId: post_login_phone + summary: Request a phone code from Telegram + tags: [Authentication] + responses: + 200: + description: Code requested successfully + schema: + $ref: "#/definitions/AuthSuccess" + 400: + description: Invalid phone number or missing Matrix ID + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: machine_readable_error + enum: + - phone_number_invalid + - mxid_empty + error: + $ref: "#/definitions/HumanReadableError" + 403: + description: Matrix ID is not whitelisted or phone number is banned or has forbidden 3rd party apps + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: machine_readable_error + enum: + - mxid_not_whitelisted + - phone_number_banned + - phone_number_app_signup_forbidden + error: + $ref: "#/definitions/HumanReadableError" + 404: + description: Unregistered phone number + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - phone_number_unoccupied + error: + $ref: "#/definitions/HumanReadableError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 429: + description: Phone number has been temporarily blocked for flooding + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - flood_wait + - phone_number_flood + error: + $ref: "#/definitions/HumanReadableError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + phone: + type: string + description: The phone number to log in as. + example: "+123456789" + /login/{mxid}/send_code: + post: + operationId: post_login_code + summary: Send the login code + tags: [Authentication] + responses: + 200: + description: Login successful + schema: + $ref: "#/definitions/AuthSuccess" + 202: + description: Correct code, but two-factor authentication is enabled + schema: + $ref: "#/definitions/AuthSuccess" + 400: + $ref: "#/responses/MissingMXIDError" + 401: + description: Invalid phone code + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - phone_code_invalid + error: + $ref: "#/definitions/HumanReadableError" + 403: + description: Matrix ID not whitelisted or phone code expired + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: machine_readable_error + enum: + - mxid_not_whitelisted + - phone_code_expired + error: + $ref: "#/definitions/HumanReadableError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + code: + type: integer + description: The phone code from Telegram. + format: int32 + example: 123456 + /login/{mxid}/send_password: + post: + operationId: post_login_password + summary: Send the two-factor auth password + tags: [Authentication] + responses: + 200: + description: Login successful + schema: + $ref: "#/definitions/AuthSuccess" + 400: + description: Missing password or Matrix ID + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: _empty + enum: + - password_empty + - mxid_empty + error: + $ref: "#/definitions/HumanReadableError" + 401: + description: Incorrect password + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - password_invalid + error: + $ref: "#/definitions/HumanReadableError" + 403: + $ref: "#/responses/NotWhitelistedError" + 409: + $ref: "#/responses/AlreadyLoggedInError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + password: + type: string + description: The two-factor auth password + format: password + example: hunter2 + +responses: + NotWhitelistedError: + description: Matrix ID not whitelisted for puppeting + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - mxid_not_whitelisted + error: + $ref: "#/definitions/HumanReadableError" + AlreadyLoggedInError: + description: The Matrix user is already logged in + schema: + type: object + properties: + state: + type: string + enum: + - logged-in + username: + type: string + description: The Telegram username the user is logged in as. + MissingMXIDError: + description: Missing Matrix ID + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - mxid_empty + error: + $ref: "#/definitions/HumanReadableError" + UnknownError: + description: Unknown error + schema: + type: object + title: UnknownError + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - exception + error: + type: string + title: Error + description: A human-readable description of the error + example: Internal server error while . + enum: + - Internal server error while requesting code. + - Internal server error while sending code. + - Internal server error while sending password. + - Internal server error while sending token. + definitions: - Error: - x-oad-type: object - type: object - title: Error - properties: - errcode: - x-oad-type: string - type: string - title: 'Error code' - description: 'A machine-readable error code' - error: - x-oad-type: string - type: string - title: Error - description: 'A human-readable description of the error' - status: - x-oad-type: integer - type: integer - title: Status - description: 'The HTTP status code' - format: int32 + HumanReadableError: + type: string + description: A human-readable description of the error + example: A human-readable description of the error AuthSuccess: - x-oad-type: object type: object properties: state: - x-oad-type: string type: string + description: The state/next step after the successful operation. enum: - - code - - request - - password - - token - - logged-in + - code + - request + - password + - token + - logged-in + username: + type: string + description: The Telegram username the user is logged in as. Only applicable if state=logged-in + + security: - - - Bearer: [] + - Bearer: [] securityDefinitions: Bearer: - description: 'Required authentication for all endpoints' + description: Required authentication for all endpoints name: Authorization in: header type: apiKey -info: - title: 'mautrix-telegram provisioning' - version: 0.3.0 - description: 'The provisioning API for mautrix-telegram.' - contact: - name: 'Tulir Asokan' - email: tulir@maunium.net - url: 'https://maunium.net' - license: - name: AGPLv3 - url: 'https://github.com/tulir/mautrix-telegram/blob/master/LICENSE' -externalDocs: - description: 'Provisioning API wiki page on GitHub.' - url: 'https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API' -basePath: /_matrix/provisioning -schemes: - - https -consumes: - - application/json -produces: - - application/json -swagger: '2.0' diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py index 89a76fd7..7a83d731 100644 --- a/mautrix_telegram/web/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -16,7 +16,6 @@ # along with this program. If not, see . from aiohttp import web from mako.template import Template -import asyncio import pkg_resources import logging From bc160e0593a3a2d26c6429b84a24631d47d07013 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 22:11:05 +0300 Subject: [PATCH 07/24] Update logger names --- mautrix_telegram/web/common/auth_api.py | 2 +- mautrix_telegram/web/provisioning/__init__.py | 2 +- mautrix_telegram/web/public/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py index 6980f1af..2ea50bca 100644 --- a/mautrix_telegram/web/common/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -26,7 +26,7 @@ from mautrix_telegram.util import format_duration class AuthAPI(abc.ABC): - log = logging.getLogger("mau.public.auth") + log = logging.getLogger("mau.web.auth") def __init__(self, loop): self.loop = loop diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 05c73816..c2f087a9 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -21,7 +21,7 @@ from ..common import AuthAPI class ProvisioningAPI(AuthAPI): - log = logging.getLogger("mau.provisioning") + log = logging.getLogger("mau.web.provisioning") def __init__(self, loop): super(AuthAPI, self).__init__(loop) diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py index 7a83d731..43738d8e 100644 --- a/mautrix_telegram/web/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -24,7 +24,7 @@ from ..common import AuthAPI class PublicBridgeWebsite(AuthAPI): - log = logging.getLogger("mau.public") + log = logging.getLogger("mau.web.public") def __init__(self, loop): super(AuthAPI, self).__init__(loop) From 48665acf1d16cc1e7abbfd17aa8b12790398508a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 22:14:04 +0300 Subject: [PATCH 08/24] Fix imports and other mistakes --- mautrix_telegram/web/provisioning/__init__.py | 3 ++- mautrix_telegram/web/public/__init__.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index c2f087a9..f5d56c5e 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -17,6 +17,7 @@ from aiohttp import web import logging +from ...user import User from ..common import AuthAPI @@ -24,7 +25,7 @@ class ProvisioningAPI(AuthAPI): log = logging.getLogger("mau.web.provisioning") def __init__(self, loop): - super(AuthAPI, self).__init__(loop) + super().__init__(loop) self.app = web.Application(loop=loop) diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py index 43738d8e..eab1f5fe 100644 --- a/mautrix_telegram/web/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -27,16 +27,16 @@ class PublicBridgeWebsite(AuthAPI): log = logging.getLogger("mau.web.public") def __init__(self, loop): - super(AuthAPI, self).__init__(loop) + super().__init__(loop) self.login = Template( - pkg_resources.resource_string("mautrix_telegram", "public/login.html.mako")) + pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako")) self.app = web.Application(loop=loop) self.app.router.add_route("GET", "/login", self.get_login) self.app.router.add_route("POST", "/login", self.post_login) - self.app.router.add_static("/", - pkg_resources.resource_filename("mautrix_telegram", "public/")) + self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_telegram", + "web/public/")) async def get_login(self, request): state = "token" if request.rel_url.query.get("mode", "") == "bot" else "request" From 5082cd1c94308be66f7b4fa07389a13828de9509 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 22:28:43 +0300 Subject: [PATCH 09/24] Fix bad JSON handling and include state in all responses --- mautrix_telegram/web/provisioning/__init__.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index f5d56c5e..909725c6 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -44,49 +44,55 @@ class ProvisioningAPI(AuthAPI): } elif message: resp = { - "message": message + "state": state, + "message": message, } else: resp = { + "state": state, "error": error, "errcode": errcode, } return web.json_response(resp, status=status) - async def get_user(self, request: web.Request): + async def get_request_info(self, request: web.Request): mxid = request.match_info["mxid"] user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) if not user.puppet_whitelisted: - return user, self.get_login_response(mxid=user.mxid, error="You are not whitelisted.", - errcode="mxid_not_whitelisted", status=403) + return None, user, self.get_login_response(mxid=user.mxid, + error="You are not whitelisted.", + errcode="mxid_not_whitelisted", status=403) elif await user.is_logged_in(): - return user, self.get_login_response(mxid=user.mxid, username=user.username, status=409) - return user, None + return None, user, self.get_login_response(mxid=user.mxid, username=user.username, + status=409) + + try: + data = await request.json() + except Exception: + return None, user, self.get_login_response(mxid=user.mxid, error="Invalid JSON.", + errcode="invalid_json", status=400) + return data, user, None async def send_bot_token(self, request: web.Request): - user, err = await self.get_user(request) + data, user, err = await self.get_request_info(request) if err: return err - data = await request.json() return await self.post_login_token(user, data.get("token", "")) async def request_code(self, request: web.Request): - user, err = await self.get_user(request) + data, user, err = await self.get_request_info(request) if err: return err - data = await request.json() return await self.post_login_phone(user, data.get("phone", "")) async def send_code(self, request: web.Request): - user, err = await self.get_user(request) + data, user, err = await self.get_request_info(request) if err: return err - data = await request.json() return await self.post_login_code(user, data.get("code", 0), password_in_data=False) async def send_password(self, request: web.Request): - user, err = await self.get_user(request) + data, user, err = await self.get_request_info(request) if err: return err - data = await request.json() return await self.post_login_password(user, data.get("password", "")) From 998e2fa19cb76ac34dc8cad83df94cf35838979b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 22:46:38 +0300 Subject: [PATCH 10/24] Enable aiohttp logging by default --- example-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example-config.yaml b/example-config.yaml index 6f9e8df5..1d5cfdb7 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -239,6 +239,8 @@ logging: level: DEBUG telethon: level: DEBUG + aiohttp: + level: INFO root: level: DEBUG handlers: [file, console] From 94a2344f3bc8a1eadde4d04cd00e5923421afe40 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 22:47:09 +0300 Subject: [PATCH 11/24] Enable and spec authorization and json validation --- mautrix_telegram/__main__.py | 2 +- mautrix_telegram/web/provisioning/__init__.py | 39 ++++++++++++------- mautrix_telegram/web/provisioning/spec.yaml | 32 ++++++++++++--- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 9702468f..c8865686 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -93,7 +93,7 @@ if config["appservice.public.enabled"]: appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public.app) if config["appservice.provisioning.enabled"]: - provisioning_api = ProvisioningAPI(loop) + provisioning_api = ProvisioningAPI(config, loop) appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning", provisioning_api.app) diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 909725c6..1c609aaf 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -16,6 +16,7 @@ # along with this program. If not, see . from aiohttp import web import logging +import json from ...user import User from ..common import AuthAPI @@ -24,8 +25,9 @@ from ..common import AuthAPI class ProvisioningAPI(AuthAPI): log = logging.getLogger("mau.web.provisioning") - def __init__(self, loop): + def __init__(self, config, loop): super().__init__(loop) + self.secret = config["appservice.provisioning.shared_secret"] self.app = web.Application(loop=loop) @@ -56,43 +58,50 @@ class ProvisioningAPI(AuthAPI): return web.json_response(resp, status=status) async def get_request_info(self, request: web.Request): + auth = request.headers.get("Authorization", "") + if auth != f"Bearer {self.secret}": + return None, None, self.get_login_response(error="Shared secret is not valid.", + errcode="shared_secret_invalid", + status=401) + + data = None + try: + data = await request.json() + except json.JSONDecodeError: + pass + if not data: + return None, None, self.get_login_response(error="Invalid JSON.", + errcode="json_invalid", status=400) + mxid = request.match_info["mxid"] user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) if not user.puppet_whitelisted: - return None, user, self.get_login_response(mxid=user.mxid, - error="You are not whitelisted.", + return None, user, self.get_login_response(error="You are not whitelisted.", errcode="mxid_not_whitelisted", status=403) elif await user.is_logged_in(): - return None, user, self.get_login_response(mxid=user.mxid, username=user.username, - status=409) - - try: - data = await request.json() - except Exception: - return None, user, self.get_login_response(mxid=user.mxid, error="Invalid JSON.", - errcode="invalid_json", status=400) + return None, user, self.get_login_response(username=user.username, status=409) return data, user, None async def send_bot_token(self, request: web.Request): data, user, err = await self.get_request_info(request) - if err: + if err is not None: return err return await self.post_login_token(user, data.get("token", "")) async def request_code(self, request: web.Request): data, user, err = await self.get_request_info(request) - if err: + if err is not None: return err return await self.post_login_phone(user, data.get("phone", "")) async def send_code(self, request: web.Request): data, user, err = await self.get_request_info(request) - if err: + if err is not None: return err return await self.post_login_code(user, data.get("code", 0), password_in_data=False) async def send_password(self, request: web.Request): data, user, err = await self.get_request_info(request) - if err: + if err is not None: return err return await self.post_login_password(user, data.get("password", "")) diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index e8fe08f3..aa911d46 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -39,7 +39,7 @@ paths: 400: $ref: "#/responses/MissingMXIDError" 401: - description: Invalid or expired bot token + description: Invalid or expired bot token or invalid shared secret schema: type: object title: Error @@ -52,6 +52,7 @@ paths: enum: - bot_token_invalid - bot_token_expired + - shared_secret_invalid error: $ref: "#/definitions/HumanReadableError" 403: @@ -87,7 +88,7 @@ paths: schema: $ref: "#/definitions/AuthSuccess" 400: - description: Invalid phone number or missing Matrix ID + description: Invalid phone number or JSON or missing Matrix ID schema: type: object title: Error @@ -100,6 +101,21 @@ paths: enum: - phone_number_invalid - mxid_empty + - json_invalid + error: + $ref: "#/definitions/HumanReadableError" + 401: + description: Invalid shared secret + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - shared_secret_invalid error: $ref: "#/definitions/HumanReadableError" 403: @@ -185,7 +201,7 @@ paths: 400: $ref: "#/responses/MissingMXIDError" 401: - description: Invalid phone code + description: Invalid phone code or shared secret schema: type: object title: Error @@ -196,6 +212,7 @@ paths: description: A machine-readable error code enum: - phone_code_invalid + - shared_secret_invalid error: $ref: "#/definitions/HumanReadableError" 403: @@ -246,7 +263,7 @@ paths: schema: $ref: "#/definitions/AuthSuccess" 400: - description: Missing password or Matrix ID + description: Missing password or Matrix ID or invalid JSON schema: type: object title: Error @@ -259,10 +276,11 @@ paths: enum: - password_empty - mxid_empty + - json_invalid error: $ref: "#/definitions/HumanReadableError" 401: - description: Incorrect password + description: Incorrect password or invalid shared secret schema: type: object title: Error @@ -273,6 +291,7 @@ paths: description: A machine-readable error code enum: - password_invalid + - shared_secret_invalid error: $ref: "#/definitions/HumanReadableError" 403: @@ -327,7 +346,7 @@ responses: type: string description: The Telegram username the user is logged in as. MissingMXIDError: - description: Missing Matrix ID + description: Missing Matrix ID or invalid JSON. schema: type: object title: Error @@ -338,6 +357,7 @@ responses: description: A machine-readable error code enum: - mxid_empty + - json_invalid error: $ref: "#/definitions/HumanReadableError" UnknownError: From ac4d7cc412440daa0a3d64287e63c8af7aad2814 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 22:58:07 +0300 Subject: [PATCH 12/24] Add /get_me endpoint --- mautrix_telegram/web/provisioning/__init__.py | 37 ++++++++--- mautrix_telegram/web/provisioning/spec.yaml | 64 +++++++++++++++++++ 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 1c609aaf..95919c58 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -31,6 +31,7 @@ class ProvisioningAPI(AuthAPI): self.app = web.Application(loop=loop) + self.app.router.add_route("GET", "/{mxid:@[^:]*:.+}/get_me", self.get_me) login_prefix = "/login/{mxid:@[^:]*:.+}" self.app.router.add_route("POST", f"{login_prefix}/bot_token", self.send_bot_token) self.app.router.add_route("POST", f"{login_prefix}/request_code", self.request_code) @@ -57,7 +58,7 @@ class ProvisioningAPI(AuthAPI): } return web.json_response(resp, status=status) - async def get_request_info(self, request: web.Request): + async def get_request_info(self, request: web.Request, get_data=True, fail_on_logged_in=True): auth = request.headers.get("Authorization", "") if auth != f"Bearer {self.secret}": return None, None, self.get_login_response(error="Shared secret is not valid.", @@ -65,23 +66,41 @@ class ProvisioningAPI(AuthAPI): status=401) data = None - try: - data = await request.json() - except json.JSONDecodeError: - pass - if not data: - return None, None, self.get_login_response(error="Invalid JSON.", - errcode="json_invalid", status=400) + if get_data: + try: + data = await request.json() + except json.JSONDecodeError: + pass + if not data: + return None, None, self.get_login_response(error="Invalid JSON.", + errcode="json_invalid", status=400) mxid = request.match_info["mxid"] user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) if not user.puppet_whitelisted: return None, user, self.get_login_response(error="You are not whitelisted.", errcode="mxid_not_whitelisted", status=403) - elif await user.is_logged_in(): + elif fail_on_logged_in and await user.is_logged_in(): return None, user, self.get_login_response(username=user.username, status=409) return data, user, None + async def get_me(self, request: web.Request): + data, user, err = await self.get_request_info(request, get_data=False, + fail_on_logged_in=False) + if err is not None: + return err + if not await user.is_logged_in(): + return self.get_login_response(status=403, error="You are not logged in.", + errcode="not_logged_in") + me = await user.client.get_me() + return web.json_response({ + "username": me.username, + "first_name": me.first_name, + "last_name": me.last_name, + "phone": me.phone, + "is_bot": me.bot, + }) + async def send_bot_token(self, request: web.Request): data, user, err = await self.get_request_info(request) if err is not None: diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index aa911d46..5766af2e 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -26,6 +26,51 @@ tags: - name: Authentication paths: + /{mxid}/get_me: + get: + operationId: get_me + summary: Get the info of the Telegram user the given Matrix user is logged in as + tags: [Authentication] + responses: + 200: + description: User is logged in + schema: + $ref: "#/definitions/AuthInfo" + 400: + $ref: "#/responses/MissingMXIDError" + 403: + description: User is not logged in or not whitelisted + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - not_logged_in + - mxid_not_whitelisted + error: + $ref: "#/definitions/HumanReadableError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: mxid + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + - name: body + in: body + required: true + schema: + type: object + properties: + token: + type: string + description: The access token of the bot to log in as + example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv" /login/{mxid}/bot_token: post: operationId: post_bot_token @@ -388,6 +433,25 @@ definitions: type: string description: A human-readable description of the error example: A human-readable description of the error + AuthInfo: + type: object + properties: + username: + type: string + example: username + first_name: + type: string + example: Usern + last_name: + type: string + example: A. + phone: + type: string + example: +123456789 + is_bot: + type: boolean + example: false + AuthSuccess: type: object properties: From f6fb37f5daa927a5b1465f34295462cd25d579b3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 22:59:26 +0300 Subject: [PATCH 13/24] Update endpoint paths --- mautrix_telegram/web/provisioning/__init__.py | 12 ++++++------ mautrix_telegram/web/provisioning/spec.yaml | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 95919c58..a792720b 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -31,12 +31,12 @@ class ProvisioningAPI(AuthAPI): self.app = web.Application(loop=loop) - self.app.router.add_route("GET", "/{mxid:@[^:]*:.+}/get_me", self.get_me) - login_prefix = "/login/{mxid:@[^:]*:.+}" - self.app.router.add_route("POST", f"{login_prefix}/bot_token", self.send_bot_token) - self.app.router.add_route("POST", f"{login_prefix}/request_code", self.request_code) - self.app.router.add_route("POST", f"{login_prefix}/send_code", self.send_code) - self.app.router.add_route("POST", f"{login_prefix}/send_password", self.send_password) + auth_prefix = "/auth/{mxid:@[^:]*:.+}" + self.app.router.add_route("GET", f"{auth_prefix}/get_me", self.get_me) + self.app.router.add_route("POST", f"{auth_prefix}/send_bot_token", self.send_bot_token) + self.app.router.add_route("POST", f"{auth_prefix}/request_code", self.request_code) + self.app.router.add_route("POST", f"{auth_prefix}/send_code", self.send_code) + self.app.router.add_route("POST", f"{auth_prefix}/send_password", self.send_password) def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", errcode=""): diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index 5766af2e..c0b5e3a6 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -26,7 +26,7 @@ tags: - name: Authentication paths: - /{mxid}/get_me: + /auth/{mxid}/get_me: get: operationId: get_me summary: Get the info of the Telegram user the given Matrix user is logged in as @@ -71,7 +71,7 @@ paths: type: string description: The access token of the bot to log in as example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv" - /login/{mxid}/bot_token: + /auth/{mxid}/send_bot_token: post: operationId: post_bot_token summary: Log in with a bot token @@ -122,7 +122,7 @@ paths: type: string description: The access token of the bot to log in as example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv" - /login/{mxid}/request_code: + /auth/{mxid}/request_code: post: operationId: post_login_phone summary: Request a phone code from Telegram @@ -229,7 +229,7 @@ paths: type: string description: The phone number to log in as. example: "+123456789" - /login/{mxid}/send_code: + /auth/{mxid}/send_code: post: operationId: post_login_code summary: Send the login code @@ -297,7 +297,7 @@ paths: description: The phone code from Telegram. format: int32 example: 123456 - /login/{mxid}/send_password: + /auth/{mxid}/send_password: post: operationId: post_login_password summary: Send the two-factor auth password From 90e7a09b7e83a78359559da0b31625e1efc71b9d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Jul 2018 23:03:34 +0300 Subject: [PATCH 14/24] Automatically generate provisioning shared secret if it has the default value --- example-config.yaml | 2 +- mautrix_telegram/config.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/example-config.yaml b/example-config.yaml index 1d5cfdb7..eb6c57d8 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -41,7 +41,7 @@ appservice: # The prefix to use in the provisioning API endpoints. prefix: /_matrix/provision # The shared secret to authorize users of the API. - # You can generate a decent secret with `pwgen -snc 32 1` + # If you leave the default token, a random token will be generated and saved at startup. shared_secret: "Very secret shared secret" # The unique ID of this appservice. diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 07944b3c..facb4806 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -162,6 +162,8 @@ class Config(DictWithRecursion): copy("appservice.provisioning.enabled") copy("appservice.provisioning.prefix") copy("appservice.provisioning.shared_secret") + if base["appservice.provisioning.shared_secret"] == "Very secret shared secret": + base["appservice.provisioning.shared_secret"] = self._new_token() copy("appservice.id") copy("appservice.bot_username") From d97281bcdc20ef9df26952430a495d7fed4a2117 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Jul 2018 16:00:20 +0300 Subject: [PATCH 15/24] Require authentication for web login. Fixes #163 --- mautrix_telegram/__main__.py | 11 +++-- mautrix_telegram/commands/auth.py | 2 +- mautrix_telegram/commands/handler.py | 7 +-- mautrix_telegram/context.py | 29 +++++++---- mautrix_telegram/formatter/__init__.py | 4 +- mautrix_telegram/formatter/from_matrix.py | 5 +- mautrix_telegram/formatter/from_telegram.py | 5 +- mautrix_telegram/util/__init__.py | 1 + mautrix_telegram/util/signed_token.py | 53 +++++++++++++++++++++ mautrix_telegram/web/public/__init__.py | 40 ++++++++++++---- mautrix_telegram/web/public/login.html.mako | 10 ++-- 11 files changed, 130 insertions(+), 37 deletions(-) create mode 100644 mautrix_telegram/util/signed_token.py diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index c8865686..1969437b 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -85,18 +85,21 @@ appserv = AppService(config["homeserver.address"], config["homeserver.domain"], config["appservice.as_token"], config["appservice.hs_token"], config["appservice.bot_username"], log="mau.as", loop=loop, verify_ssl=config["homeserver.verify_ssl"]) - -context = Context(appserv, db_session, config, loop, None, None, session_container) +public_website = None +provisioning_api = None if config["appservice.public.enabled"]: - public = PublicBridgeWebsite(loop) - appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public.app) + public_website = PublicBridgeWebsite(loop) + appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public_website.app) if config["appservice.provisioning.enabled"]: provisioning_api = ProvisioningAPI(config, loop) appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning", provisioning_api.app) +context = Context(appserv, db_session, config, loop, None, None, session_container, public_website, + provisioning_api) + with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start: init_db(db_session) init_abstract_user(context) diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py index 8caf21a5..b5eaf1d9 100644 --- a/mautrix_telegram/commands/auth.py +++ b/mautrix_telegram/commands/auth.py @@ -114,7 +114,7 @@ async def login(evt: CommandEvent): if evt.config["appservice.public.enabled"]: prefix = evt.config["appservice.public.external"] - url = f"{prefix}/login?mxid={evt.sender.mxid}" + url = f"{prefix}/login?token={evt.public_website.make_token(evt.sender.mxid)}" if evt.config.get("bridge.allow_matrix_login", True): return await evt.reply( "This bridge instance allows you to log in inside or outside Matrix.\n\n" diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 7bf4323d..53a71a4b 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -22,8 +22,7 @@ import logging from telethon.errors import FloodWaitError from ..util import format_duration -from ..context import Context -from .. import user as u +from .. import user as u, context as c command_handlers = {} # type: Dict[str, CommandHandler] @@ -45,6 +44,7 @@ class CommandEvent: self.loop = processor.loop self.tgbot = processor.tgbot self.config = processor.config + self.public_website = processor.public_website self.command_prefix = processor.command_prefix self.room_id = room self.sender = sender @@ -131,8 +131,9 @@ def command_handler(_func: Optional[Callable[[CommandEvent], None]] = None, *, n class CommandProcessor: log = logging.getLogger("mau.commands") - def __init__(self, context: Context): + def __init__(self, context: c.Context): self.az, self.db, self.config, self.loop, self.tgbot = context + self.public_website = context.public_website self.command_prefix = self.config["bridge.command_prefix"] async def handle(self, room: str, sender: u.User, command: str, args: List[str], diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py index dfb32b59..ad48d7e4 100644 --- a/mautrix_telegram/context.py +++ b/mautrix_telegram/context.py @@ -14,17 +14,30 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import asyncio + +from sqlalchemy.orm import scoped_session +from alchemysession import AlchemySessionContainer +from mautrix_appservice import AppService class Context: - def __init__(self, az, db, config, loop, bot, mx, session_container): - self.az = az - self.db = db - self.config = config - self.loop = loop - self.bot = bot - self.mx = mx - self.session_container = session_container + def __init__(self, az, db, config, loop, bot, mx, session_container, public_website, + provisioning_api): + from .web import PublicBridgeWebsite, ProvisioningAPI + from .config import Config + from .bot import Bot + from .matrix import MatrixHandler + + self.az = az # type: AppService + self.db = db # type: scoped_session + self.config = config # type: Config + self.loop = loop # type: asyncio.AbstractEventLoop + self.bot = bot # type: Bot + self.mx = mx # type: MatrixHandler + self.session_container = session_container # type: AlchemySessionContainer + self.public_website = public_website # type: PublicBridgeWebsite + self.provisioning_api = provisioning_api # type: ProvisioningAPI def __iter__(self): yield self.az diff --git a/mautrix_telegram/formatter/__init__.py b/mautrix_telegram/formatter/__init__.py index 7cb102f7..51802ebb 100644 --- a/mautrix_telegram/formatter/__init__.py +++ b/mautrix_telegram/formatter/__init__.py @@ -1,9 +1,9 @@ from .from_matrix import (matrix_reply_to_telegram, matrix_to_telegram, matrix_text_to_telegram, init_mx) from .from_telegram import (telegram_reply_to_matrix, telegram_to_matrix, init_tg) -from ..context import Context +from .. import context as c -def init(context: Context): +def init(context: c.Context): init_mx(context) init_tg(context) diff --git a/mautrix_telegram/formatter/from_matrix.py b/mautrix_telegram/formatter/from_matrix.py index 145177fc..f98d3ad5 100644 --- a/mautrix_telegram/formatter/from_matrix.py +++ b/mautrix_telegram/formatter/from_matrix.py @@ -27,8 +27,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, M MessageEntityItalic, MessageEntityCode, MessageEntityPre, MessageEntityBotCommand, TypeMessageEntity) -from ..context import Context -from .. import user as u, puppet as pu, portal as po +from .. import user as u, puppet as pu, portal as po, context as c from ..db import Message as DBMessage from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html, trim_reply_fallback_text, html_to_unicode) @@ -352,7 +351,7 @@ def plain_mention_to_text() -> Tuple[List[TypeMessageEntity], Callable[[str], st return entities, replacer -def init_mx(context: Context): +def init_mx(context: c.Context): global plain_mention_regex, should_bridge_plaintext_highlights config = context.config dn_template = config.get("bridge.displayname_template", "{displayname} (Telegram)") diff --git a/mautrix_telegram/formatter/from_telegram.py b/mautrix_telegram/formatter/from_telegram.py index 3e9992e4..70a13a55 100644 --- a/mautrix_telegram/formatter/from_telegram.py +++ b/mautrix_telegram/formatter/from_telegram.py @@ -33,8 +33,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, from mautrix_appservice import MatrixRequestError from mautrix_appservice.intent_api import IntentAPI -from .. import user as u, puppet as pu, portal as po -from ..context import Context +from .. import user as u, puppet as pu, portal as po, context as c from ..db import Message as DBMessage from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html, trim_reply_fallback_text, unicode_to_html) @@ -321,6 +320,6 @@ def _parse_url(html: List[str], entity_text: str, url: str) -> bool: return False -def init_tg(context: Context): +def init_tg(context: c.Context): global should_highlight_edits should_highlight_edits = htmldiff and context.config["bridge.highlight_edits"] diff --git a/mautrix_telegram/util/__init__.py b/mautrix_telegram/util/__init__.py index 7d431396..99cdee2a 100644 --- a/mautrix_telegram/util/__init__.py +++ b/mautrix_telegram/util/__init__.py @@ -1,2 +1,3 @@ from .file_transfer import transfer_file_to_matrix, convert_image from .format_duration import format_duration +from .signed_token import sign_token, verify_token diff --git a/mautrix_telegram/util/signed_token.py b/mautrix_telegram/util/signed_token.py new file mode 100644 index 00000000..13281012 --- /dev/null +++ b/mautrix_telegram/util/signed_token.py @@ -0,0 +1,53 @@ +# -*- coding: future_fstrings -*- +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 +import json +import base64 +import hashlib + + +def _get_checksum(key: str, payload: bytes) -> str: + hasher = hashlib.sha256() + hasher.update(payload) + hasher.update(key.encode("utf-8")) + checksum = hasher.hexdigest() + return checksum + + +def sign_token(key: str, payload: dict) -> str: + payload = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8")) + checksum = _get_checksum(key, payload) + return f"{checksum}:{payload.decode('utf-8')}" + + +def verify_token(key: str, data: str) -> Optional[dict]: + if not data: + return None + + try: + checksum, payload = data.split(":", 1) + except ValueError: + return None + + if checksum != _get_checksum(key, payload.encode("utf-8")): + return None + + payload = base64.urlsafe_b64decode(payload).decode("utf-8") + try: + return json.loads(payload) + except json.JSONDecodeError: + return None diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py index eab1f5fe..fb5f6de7 100644 --- a/mautrix_telegram/web/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -18,7 +18,11 @@ from aiohttp import web from mako.template import Template import pkg_resources import logging +import random +import string +import time +from ...util import sign_token, verify_token from ...user import User from ..common import AuthAPI @@ -28,6 +32,8 @@ class PublicBridgeWebsite(AuthAPI): def __init__(self, loop): super().__init__(loop) + self.secret_key = "".join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(64)) self.login = Template( pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako")) @@ -38,10 +44,24 @@ class PublicBridgeWebsite(AuthAPI): self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_telegram", "web/public/")) - async def get_login(self, request): - state = "token" if request.rel_url.query.get("mode", "") == "bot" else "request" + def make_token(self, mxid, expires_in=900): + return sign_token(self.secret_key, { + "mxid": mxid, + "expiry": int(time.time()) + expires_in, + }) - mxid = request.rel_url.query.get("mxid", None) + def verify_token(self, token): + token = verify_token(self.secret_key, token) + if token and token.get("expiry", 0) > int(time.time()): + return token.get("mxid", None) + return None + + async def get_login(self, request): + state = "bot_token" if request.rel_url.query.get("mode", "") == "bot" else "request" + + mxid = self.verify_token(request.rel_url.query.get("token", None)) + if not mxid: + return self.get_login_response(status=401, state="invalid-token") user = User.get_by_mxid(mxid, create=False) if mxid else None if not user: @@ -62,11 +82,13 @@ class PublicBridgeWebsite(AuthAPI): message=message, mxid=mxid)) async def post_login(self, request): - data = await request.post() - if "mxid" not in data: - return self.get_login_response(error="Please enter your Matrix ID.", status=400) + mxid = self.verify_token(request.rel_url.query.get("token", None)) + if not mxid: + return self.get_login_response(status=401, state="invalid-token") - user = await User.get_by_mxid(data["mxid"]).ensure_started(even_if_no_session=True) + data = await request.post() + + user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) if not user.puppet_whitelisted: return self.get_login_response(mxid=user.mxid, error="You are not whitelisted.", status=403) @@ -77,8 +99,8 @@ class PublicBridgeWebsite(AuthAPI): if "phone" in data: return await self.post_login_phone(user, data["phone"]) - elif "token" in data: - return await self.post_login_token(user, data["token"]) + elif "bot_token" in data: + return await self.post_login_token(user, data["bot_token"]) elif "code" in data: resp = await self.post_login_code(user, data["code"], password_in_data="password" in data) diff --git a/mautrix_telegram/web/public/login.html.mako b/mautrix_telegram/web/public/login.html.mako index 8c03cbdc..f00b6a69 100644 --- a/mautrix_telegram/web/public/login.html.mako +++ b/mautrix_telegram/web/public/login.html.mako @@ -76,6 +76,9 @@ along with this program. If not, see . management command first.

% endif + % elif state == "invalid-token": +

Invalid or expired token

+
Please ask the bridge bot for a new login link.
% else:

Log in to Telegram

% if error: @@ -87,8 +90,7 @@ along with this program. If not, see .
- + % if state == "request": @@ -96,9 +98,9 @@ along with this program. If not, see . - % elif state == "token": + % elif state == "bot_token": - + % elif state == "code": From 2b5426fda394877f80e08d3442f4f34337d9e8f4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Jul 2018 18:57:46 +0300 Subject: [PATCH 16/24] Add portal info and user chat list endpoints --- mautrix_telegram/abstract_user.py | 4 +- mautrix_telegram/user.py | 15 +- mautrix_telegram/web/provisioning/__init__.py | 207 +++++++++++----- mautrix_telegram/web/provisioning/spec.yaml | 228 ++++++++++++++++-- 4 files changed, 362 insertions(+), 92 deletions(-) diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index d15e9e66..1095e993 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -96,9 +96,9 @@ class AbstractUser: except Exception: self.log.exception("Failed to handle Telegram update") - async def _get_dialogs(self, limit=None): + async def get_dialogs(self, limit=None) -> List[Union[Chat, Channel]]: if self.is_bot: - return + return [] dialogs = await self.client.get_dialogs(limit=limit) return [dialog.entity for dialog in dialogs if ( not isinstance(dialog.entity, (User, ChatForbidden, ChannelForbidden)) diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index da62fad8..917cbf73 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Dict import logging import asyncio import re @@ -38,18 +39,18 @@ class User(AbstractUser): def __init__(self, mxid, tgid=None, username=None, db_contacts=None, saved_contacts=0, is_bot=False, db_portals=None, db_instance=None): super().__init__() - self.mxid = mxid - self.tgid = tgid - self.is_bot = is_bot - self.username = username + self.mxid = mxid # type: str + self.tgid = tgid # type: int + self.is_bot = is_bot # type: bool + self.username = username # type: str self.contacts = [] self.saved_contacts = saved_contacts self.db_contacts = db_contacts - self.portals = {} + self.portals = {} # type: Dict[str, po.Portal] self.db_portals = db_portals self._db_instance = db_instance - self.command_status = None + self.command_status = None # type: dict (self.relaybot_whitelisted, self.whitelisted, @@ -255,7 +256,7 @@ class User(AbstractUser): async def sync_dialogs(self, synchronous_create=False): creators = [] - for entity in await self._get_dialogs(limit=30): + for entity in await self.get_dialogs(limit=30): portal = po.Portal.get_by_entity(entity) self.portals[portal.tgid_full] = portal creators.append( diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index a792720b..becee3a2 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -15,10 +15,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from aiohttp import web +from typing import Tuple, Optional, Callable, Awaitable import logging import json +from telethon.utils import get_peer_id + from ...user import User +from ...portal import Portal from ..common import AuthAPI @@ -29,17 +33,117 @@ class ProvisioningAPI(AuthAPI): super().__init__(loop) self.secret = config["appservice.provisioning.shared_secret"] - self.app = web.Application(loop=loop) + self.app = web.Application(loop=loop, middlewares=[self.error_middleware]) - auth_prefix = "/auth/{mxid:@[^:]*:.+}" - self.app.router.add_route("GET", f"{auth_prefix}/get_me", self.get_me) - self.app.router.add_route("POST", f"{auth_prefix}/send_bot_token", self.send_bot_token) - self.app.router.add_route("POST", f"{auth_prefix}/request_code", self.request_code) - self.app.router.add_route("POST", f"{auth_prefix}/send_code", self.send_code) - self.app.router.add_route("POST", f"{auth_prefix}/send_password", self.send_password) + portal_prefix = "/portal/{mxid:![^/]+}" + self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal) + # self.app.router.add_route("POST", portal_prefix + "/connect/{chat_id:[0-9]+}", + # self.connect_chat) + # self.app.router.add_route("POST", f"{portal_prefix}/create", self.create_chat) + # self.app.router.add_route("POST", f"{portal_prefix}/disconnect", self.disconnect_chat) + + user_prefix = "/user/{mxid:@[^:]*:[^/]+}" + self.app.router.add_route("GET", f"{user_prefix}", self.get_me) + self.app.router.add_route("GET", f"{user_prefix}/chats", self.get_chats) + + self.app.router.add_route("POST", f"{user_prefix}/send_bot_token", self.send_bot_token) + self.app.router.add_route("POST", f"{user_prefix}/request_code", self.request_code) + self.app.router.add_route("POST", f"{user_prefix}/send_code", self.send_code) + self.app.router.add_route("POST", f"{user_prefix}/send_password", self.send_password) + + async def get_portal(self, request: web.Request) -> web.Response: + mxid = request.match_info["mxid"] + portal = Portal.get_by_mxid(mxid) + if not portal: + return self.get_error_response(404, "room_not_found", + "Portal with given Matrix ID not found.") + return web.json_response({ + "mxid": portal.mxid, + "chat_id": get_peer_id(portal.peer), + "peer_type": portal.peer_type, + "title": portal.title, + "about": portal.about, + "username": portal.username, + "megagroup": portal.megagroup, + }) + + async def get_me(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request, require_logged_in=True) + if err is not None: + return err + + me = await user.client.get_me() + return web.json_response({ + "username": me.username, + "first_name": me.first_name, + "last_name": me.last_name, + "phone": me.phone, + "is_bot": me.bot, + }) + + async def get_chats(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request, require_logged_in=True) + if err is not None: + return err + + if not user.is_bot: + chats = await user.get_dialogs() + return web.json_response([{ + "id": get_peer_id(chat), + "title": chat.title, + } for chat in chats]) + else: + return web.json_response([{ + "id": get_peer_id(chat.peer), + "title": chat.title, + } for chat in user.portals.values() if chat.tgid]) + + async def send_bot_token(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request) + if err is not None: + return err + return await self.post_login_token(user, data.get("token", "")) + + async def request_code(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request) + if err is not None: + return err + return await self.post_login_phone(user, data.get("phone", "")) + + async def send_code(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request) + if err is not None: + return err + return await self.post_login_code(user, data.get("code", 0), password_in_data=False) + + async def send_password(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request) + if err is not None: + return err + return await self.post_login_password(user, data.get("password", "")) + + @staticmethod + async def error_middleware(_, handler) -> Callable[[web.Request], Awaitable[web.Response]]: + async def middleware_handler(request: web.Request) -> web.Response: + try: + return await handler(request) + except web.HTTPException as ex: + return web.json_response({ + "error": f"Unhandled HTTP {ex.status}", + "errcode": f"unhandled_http_{ex.status}", + }, status=ex.status) + + return middleware_handler + + @staticmethod + def get_error_response(status=200, errcode="", error="") -> web.Response: + return web.json_response({ + "error": error, + "errcode": errcode, + }, status=status) def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", - errcode=""): + errcode="") -> web.Response: if username: resp = { "state": "logged-in", @@ -58,7 +162,36 @@ class ProvisioningAPI(AuthAPI): } return web.json_response(resp, status=status) - async def get_request_info(self, request: web.Request, get_data=True, fail_on_logged_in=True): + def check_authorization(self, request: web.Request) -> bool: + return request.headers.get("Authorization", "") == f"Bearer {self.secret}" + + @staticmethod + async def get_data(request: web.Request) -> Optional[dict]: + try: + return await request.json() + except json.JSONDecodeError: + return None + + async def get_user(self, mxid: str, require_logged_in: bool = False + ) -> Tuple[Optional[User], Optional[web.Response]]: + user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) + if not user.puppet_whitelisted: + return user, self.get_login_response(error="You are not whitelisted.", + errcode="mxid_not_whitelisted", status=403) + logged_in = await user.is_logged_in() + if not require_logged_in and logged_in: + return user, self.get_login_response(username=user.username, status=409, + error="You are already logged in.", + errcode="already_logged_in") + elif require_logged_in and not logged_in: + return user, self.get_login_response(status=403, error="You are not logged in.", + errcode="not_logged_in") + return user, None + + async def get_user_request_info(self, request: web.Request, require_logged_in: bool = False + ) -> (Tuple[Optional[dict], + Optional[User], + Optional[web.Response]]): auth = request.headers.get("Authorization", "") if auth != f"Bearer {self.secret}": return None, None, self.get_login_response(error="Shared secret is not valid.", @@ -66,61 +199,13 @@ class ProvisioningAPI(AuthAPI): status=401) data = None - if get_data: - try: - data = await request.json() - except json.JSONDecodeError: - pass + if request.method == "POST" or request.method == "PUT": + data = await self.get_data(request) if not data: return None, None, self.get_login_response(error="Invalid JSON.", errcode="json_invalid", status=400) mxid = request.match_info["mxid"] - user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) - if not user.puppet_whitelisted: - return None, user, self.get_login_response(error="You are not whitelisted.", - errcode="mxid_not_whitelisted", status=403) - elif fail_on_logged_in and await user.is_logged_in(): - return None, user, self.get_login_response(username=user.username, status=409) - return data, user, None + user, err = await self.get_user(mxid, require_logged_in) - async def get_me(self, request: web.Request): - data, user, err = await self.get_request_info(request, get_data=False, - fail_on_logged_in=False) - if err is not None: - return err - if not await user.is_logged_in(): - return self.get_login_response(status=403, error="You are not logged in.", - errcode="not_logged_in") - me = await user.client.get_me() - return web.json_response({ - "username": me.username, - "first_name": me.first_name, - "last_name": me.last_name, - "phone": me.phone, - "is_bot": me.bot, - }) - - async def send_bot_token(self, request: web.Request): - data, user, err = await self.get_request_info(request) - if err is not None: - return err - return await self.post_login_token(user, data.get("token", "")) - - async def request_code(self, request: web.Request): - data, user, err = await self.get_request_info(request) - if err is not None: - return err - return await self.post_login_phone(user, data.get("phone", "")) - - async def send_code(self, request: web.Request): - data, user, err = await self.get_request_info(request) - if err is not None: - return err - return await self.post_login_code(user, data.get("code", 0), password_in_data=False) - - async def send_password(self, request: web.Request): - data, user, err = await self.get_request_info(request) - if err is not None: - return err - return await self.post_login_password(user, data.get("password", "")) + return data, user, err diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index c0b5e3a6..d3bd2ba8 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -4,16 +4,12 @@ info: title: mautrix-telegram provisioning version: 0.3.0 description: The provisioning API for mautrix-telegram. - contact: - name: Tulir Asokan - email: tulir@maunium.net - url: https://maunium.net license: name: AGPLv3 url: https://github.com/tulir/mautrix-telegram/blob/master/LICENSE externalDocs: - description: Provisioning API wiki page on GitHub. + description: Provisioning API wiki page on GitHub url: https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API basePath: /_matrix/provision @@ -23,14 +19,141 @@ consumes: [application/json] produces: [application/json] tags: +- name: User info - name: Authentication +- name: Bridging paths: - /auth/{mxid}/get_me: + /portal/{room_id}: + get: + operationId: get_portal + summary: Get the bridging status and info of the connected Telegram chat + tags: [Bridging] + responses: + 200: + description: Room is bridged + schema: + $ref: "#/definitions/PortalInfo" + 400: + $ref: "#/responses/MissingMXIDError" + 404: + description: Unknown portal + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - portal_not_found + error: + $ref: "#/definitions/HumanReadableError" + parameters: + - name: room_id + in: path + description: The Matrix ID of the room whose bridging status to get + required: true + type: string + /portal/{room_id}/connect/{chat_id}: + post: + operationId: connect_portal + summary: Connect an existing Telegram chat to the given room + tags: [Bridging] + parameters: + - name: room_id + in: path + description: The Matrix ID of the room to which the Telegram chat should be connected + required: true + type: string + - name: chat_id + in: path + description: The ID of the Telegram chat to connect + required: true + type: integer + format: int32 + responses: + 400: + $ref: "#/responses/MissingMXIDError" + 409: + description: Room is already bridged + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - room_already_bridged + error: + $ref: "#/definitions/HumanReadableError" + /portal/{room_id}/create: + post: + operationId: create_portal + summary: Create a new Telegram chat for the given room + tags: [Bridging] + responses: + 400: + $ref: "#/responses/MissingMXIDError" + 409: + description: Room is already bridged + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - room_already_bridged + error: + $ref: "#/definitions/HumanReadableError" + parameters: + - name: room_id + in: path + description: The Matrix ID of the room whose bridging status to get + required: true + type: string + /portal/{room_id}/disconnect: + post: + operationId: disconnect_portal + summary: Disconnect the Telegram chat from the room + tags: [Bridging] + responses: + 202: + description: Room unbridging initiated + 400: + $ref: "#/responses/MissingMXIDError" + 404: + description: Unknown portal + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - portal_not_found + error: + $ref: "#/definitions/HumanReadableError" + parameters: + - name: room_id + in: path + description: The Matrix ID of the room whose bridging status to get + required: true + type: string + + /user/{user_id}: get: operationId: get_me summary: Get the info of the Telegram user the given Matrix user is logged in as - tags: [Authentication] + tags: [User info] responses: 200: description: User is logged in @@ -56,22 +179,48 @@ paths: 500: $ref: "#/responses/UnknownError" parameters: - - name: mxid + - name: user_id in: path description: The Matrix ID of the user who to log in as required: true type: string - - name: body - in: body - required: true + /user/{user_id}/chats: + get: + operationId: get_chats + summary: Get the list of Telegram chats the given Matrix user has access to + tags: [User info] + responses: + 200: + description: User is logged in + schema: + $ref: "#/definitions/UserChats" + 400: + $ref: "#/responses/MissingMXIDError" + 403: + description: User is not logged in or not whitelisted schema: type: object + title: Error properties: - token: + errcode: type: string - description: The access token of the bot to log in as - example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv" - /auth/{mxid}/send_bot_token: + title: Error code + description: A machine-readable error code + enum: + - not_logged_in + - mxid_not_whitelisted + error: + $ref: "#/definitions/HumanReadableError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: user_id + in: path + description: The Matrix ID of the user who to log in as + required: true + type: string + + /user/{user_id}/login/bot_token: post: operationId: post_bot_token summary: Log in with a bot token @@ -107,7 +256,7 @@ paths: 500: $ref: "#/responses/UnknownError" parameters: - - name: mxid + - name: user_id in: path description: The Matrix ID of the user who to log in as required: true @@ -122,7 +271,7 @@ paths: type: string description: The access token of the bot to log in as example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv" - /auth/{mxid}/request_code: + /user/{user_id}/login/request_code: post: operationId: post_login_phone summary: Request a phone code from Telegram @@ -214,7 +363,7 @@ paths: 500: $ref: "#/responses/UnknownError" parameters: - - name: mxid + - name: user_id in: path description: The Matrix ID of the user who to log in as required: true @@ -229,7 +378,7 @@ paths: type: string description: The phone number to log in as. example: "+123456789" - /auth/{mxid}/send_code: + /user/{user_id}/login/send_code: post: operationId: post_login_code summary: Send the login code @@ -281,7 +430,7 @@ paths: 500: $ref: "#/responses/UnknownError" parameters: - - name: mxid + - name: user_id in: path description: The Matrix ID of the user who to log in as required: true @@ -297,7 +446,7 @@ paths: description: The phone code from Telegram. format: int32 example: 123456 - /auth/{mxid}/send_password: + /user/{user_id}/login/send_password: post: operationId: post_login_password summary: Send the two-factor auth password @@ -346,7 +495,7 @@ paths: 500: $ref: "#/responses/UnknownError" parameters: - - name: mxid + - name: user_id in: path description: The Matrix ID of the user who to log in as required: true @@ -451,6 +600,41 @@ definitions: is_bot: type: boolean example: false + UserChats: + type: array + items: + type: object + properties: + id: + type: integer + example: -123456789 + description: A bot API style chat ID. + title: + type: string + + PortalInfo: + type: object + properties: + mxid: + type: string + example: "!foo:example.com" + chat_id: + type: integer + example: -100123456789 + peer_type: + type: string + enum: + - user + - chat + - channel + megagroup: + type: boolean + username: + type: string + title: + type: string + about: + type: string AuthSuccess: type: object From bbc7912a497b29f1aa8f14c67cd721046a85ae28 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Jul 2018 19:24:05 +0300 Subject: [PATCH 17/24] Allow getting user info of unauthenticated users and add /portal/{chat_id} --- mautrix_telegram/config.py | 2 +- mautrix_telegram/user.py | 3 +- mautrix_telegram/web/provisioning/__init__.py | 86 ++++++++---- mautrix_telegram/web/provisioning/spec.yaml | 125 +++++++++++++----- 4 files changed, 157 insertions(+), 59 deletions(-) diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index facb4806..77cc6bfb 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -250,7 +250,7 @@ class Config(DictWithRecursion): puppeting = level == "full" or admin user = level == "user" or puppeting relaybot = level == "relaybot" or user - return relaybot, user, puppeting, admin + return relaybot, user, puppeting, admin, level def get_permissions(self, mxid): permissions = self["bridge.permissions"] or {} diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 917cbf73..1163e503 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -55,7 +55,8 @@ class User(AbstractUser): (self.relaybot_whitelisted, self.whitelisted, self.puppet_whitelisted, - self.is_admin) = config.get_permissions(self.mxid) + self.is_admin, + self.permissions) = config.get_permissions(self.mxid) self.by_mxid[mxid] = self if tgid: diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index becee3a2..dc8feace 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -36,14 +36,15 @@ class ProvisioningAPI(AuthAPI): self.app = web.Application(loop=loop, middlewares=[self.error_middleware]) portal_prefix = "/portal/{mxid:![^/]+}" - self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal) + self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal_by_mxid) + self.app.router.add_route("GET", "/portal/{tgid:-[0-9]+}", self.get_portal_by_tgid) # self.app.router.add_route("POST", portal_prefix + "/connect/{chat_id:[0-9]+}", # self.connect_chat) # self.app.router.add_route("POST", f"{portal_prefix}/create", self.create_chat) # self.app.router.add_route("POST", f"{portal_prefix}/disconnect", self.disconnect_chat) user_prefix = "/user/{mxid:@[^:]*:[^/]+}" - self.app.router.add_route("GET", f"{user_prefix}", self.get_me) + self.app.router.add_route("GET", f"{user_prefix}", self.get_user_info) self.app.router.add_route("GET", f"{user_prefix}/chats", self.get_chats) self.app.router.add_route("POST", f"{user_prefix}/send_bot_token", self.send_bot_token) @@ -51,11 +52,11 @@ class ProvisioningAPI(AuthAPI): self.app.router.add_route("POST", f"{user_prefix}/send_code", self.send_code) self.app.router.add_route("POST", f"{user_prefix}/send_password", self.send_password) - async def get_portal(self, request: web.Request) -> web.Response: + async def get_portal_by_mxid(self, request: web.Request) -> web.Response: mxid = request.match_info["mxid"] portal = Portal.get_by_mxid(mxid) if not portal: - return self.get_error_response(404, "room_not_found", + return self.get_error_response(404, "portal_not_found", "Portal with given Matrix ID not found.") return web.json_response({ "mxid": portal.mxid, @@ -67,22 +68,53 @@ class ProvisioningAPI(AuthAPI): "megagroup": portal.megagroup, }) - async def get_me(self, request: web.Request) -> web.Response: - data, user, err = await self.get_user_request_info(request, require_logged_in=True) + async def get_portal_by_tgid(self, request: web.Request) -> web.Response: + try: + tgid = int(request.match_info["tgid"]) + except ValueError: + return self.get_error_response(400, "tgid_invalid", + "Given chat ID is not an integer.") + + portal = Portal.get_by_tgid(tgid) + if not portal: + return self.get_error_response(404, "portal_not_found", + "Portal to given Telegram chat not found.") + return web.json_response({ + "mxid": portal.mxid, + "chat_id": get_peer_id(portal.peer), + "peer_type": portal.peer_type, + "title": portal.title, + "about": portal.about, + "username": portal.username, + "megagroup": portal.megagroup, + }) + + async def get_user_info(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request, expect_logged_in=None, + require_puppeting=False) if err is not None: return err - me = await user.client.get_me() + user_data = None + if await user.is_logged_in(): + me = await user.client.get_me() + await user.update_info(me) + user_data = { + "id": user.tgid, + "username": user.username, + "first_name": me.first_name, + "last_name": me.last_name, + "phone": me.phone, + "is_bot": user.is_bot, + } return web.json_response({ - "username": me.username, - "first_name": me.first_name, - "last_name": me.last_name, - "phone": me.phone, - "is_bot": me.bot, + "telegram": user_data, + "mxid": user.mxid, + "permissions": user.permissions, }) async def get_chats(self, request: web.Request) -> web.Response: - data, user, err = await self.get_user_request_info(request, require_logged_in=True) + data, user, err = await self.get_user_request_info(request, expect_logged_in=True) if err is not None: return err @@ -172,23 +204,27 @@ class ProvisioningAPI(AuthAPI): except json.JSONDecodeError: return None - async def get_user(self, mxid: str, require_logged_in: bool = False + async def get_user(self, mxid: str, expect_logged_in: Optional[bool] = False, + require_puppeting: bool = True, ) -> Tuple[Optional[User], Optional[web.Response]]: user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) - if not user.puppet_whitelisted: + if require_puppeting and not user.puppet_whitelisted: return user, self.get_login_response(error="You are not whitelisted.", errcode="mxid_not_whitelisted", status=403) - logged_in = await user.is_logged_in() - if not require_logged_in and logged_in: - return user, self.get_login_response(username=user.username, status=409, - error="You are already logged in.", - errcode="already_logged_in") - elif require_logged_in and not logged_in: - return user, self.get_login_response(status=403, error="You are not logged in.", - errcode="not_logged_in") + if expect_logged_in is not None: + logged_in = await user.is_logged_in() + if not expect_logged_in and logged_in: + return user, self.get_login_response(username=user.username, status=409, + error="You are already logged in.", + errcode="already_logged_in") + elif expect_logged_in and not logged_in: + return user, self.get_login_response(status=403, error="You are not logged in.", + errcode="not_logged_in") return user, None - async def get_user_request_info(self, request: web.Request, require_logged_in: bool = False + async def get_user_request_info(self, request: web.Request, + expect_logged_in: Optional[bool] = False, + require_puppeting: bool = False, ) -> (Tuple[Optional[dict], Optional[User], Optional[web.Response]]): @@ -206,6 +242,6 @@ class ProvisioningAPI(AuthAPI): errcode="json_invalid", status=400) mxid = request.match_info["mxid"] - user, err = await self.get_user(mxid, require_logged_in) + user, err = await self.get_user(mxid, expect_logged_in, require_puppeting) return data, user, err diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index d3bd2ba8..775821b9 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -56,6 +56,52 @@ paths: description: The Matrix ID of the room whose bridging status to get required: true type: string + pattern: "![^/]+" + /portal/{chat_id}: + get: + operationId: get_portal_by_tgid + summary: Get the bridging status and info of the connected Telegram chat + tags: [Bridging] + responses: + 200: + description: Chat is bridged + schema: + $ref: "#/definitions/PortalInfo" + 400: + description: Invalid Telegram chat ID + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - tgid_invalid + error: + $ref: "#/definitions/HumanReadableError" + 404: + description: Unknown portal + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - portal_not_found + error: + $ref: "#/definitions/HumanReadableError" + parameters: + - name: chat_id + in: path + description: The Matrix ID of the room whose bridging status to get + required: true + type: integer + pattern: "-[0-9]+" /portal/{room_id}/connect/{chat_id}: post: operationId: connect_portal @@ -72,12 +118,20 @@ paths: description: The ID of the Telegram chat to connect required: true type: integer - format: int32 + pattern: "-[0-9]+" + - name: force + in: query + description: Set to force bridging by unbridging or deleting existing portal rooms. + required: false + type: string + enum: + - delete + - unbridge responses: 400: $ref: "#/responses/MissingMXIDError" 409: - description: Room is already bridged + description: Matrix room or Telegram chat is already bridged schema: type: object title: Error @@ -86,8 +140,10 @@ paths: type: string title: Error code description: A machine-readable error code + example: _already_bridged enum: - room_already_bridged + - chat_already_bridged error: $ref: "#/definitions/HumanReadableError" /portal/{room_id}/create: @@ -156,26 +212,13 @@ paths: tags: [User info] responses: 200: - description: User is logged in + description: User found schema: - $ref: "#/definitions/AuthInfo" + $ref: "#/definitions/UserInfo" 400: $ref: "#/responses/MissingMXIDError" 403: - description: User is not logged in or not whitelisted - schema: - type: object - title: Error - properties: - errcode: - type: string - title: Error code - description: A machine-readable error code - enum: - - not_logged_in - - mxid_not_whitelisted - error: - $ref: "#/definitions/HumanReadableError" + $ref: "#/responses/NotWhitelistedError" 500: $ref: "#/responses/UnknownError" parameters: @@ -582,24 +625,42 @@ definitions: type: string description: A human-readable description of the error example: A human-readable description of the error - AuthInfo: + UserInfo: type: object properties: - username: + mxid: type: string - example: username - first_name: + example: "@usern:example.com" + permissions: type: string - example: Usern - last_name: - type: string - example: A. - phone: - type: string - example: +123456789 - is_bot: - type: boolean - example: false + example: user + enum: + - none + - relaybot + - user + - full + - admin + telegram: + type: object + properties: + id: + type: integer + example: 123456789 + username: + type: string + example: username + first_name: + type: string + example: Usern + last_name: + type: string + example: A. + phone: + type: string + example: +123456789 + is_bot: + type: boolean + example: false UserChats: type: array items: From 34cc810d620dca72f0a9ea5e388f88ebdfab6f3a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Jul 2018 19:33:55 +0300 Subject: [PATCH 18/24] Fix /portal/{chat_id} --- mautrix_telegram/web/provisioning/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index dc8feace..e3fa8347 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -19,7 +19,7 @@ from typing import Tuple, Optional, Callable, Awaitable import logging import json -from telethon.utils import get_peer_id +from telethon.utils import get_peer_id, resolve_id from ...user import User from ...portal import Portal @@ -70,11 +70,10 @@ class ProvisioningAPI(AuthAPI): async def get_portal_by_tgid(self, request: web.Request) -> web.Response: try: - tgid = int(request.match_info["tgid"]) + tgid, _ = resolve_id(int(request.match_info["tgid"])) except ValueError: return self.get_error_response(400, "tgid_invalid", - "Given chat ID is not an integer.") - + "Given chat ID is not valid.") portal = Portal.get_by_tgid(tgid) if not portal: return self.get_error_response(404, "portal_not_found", From 4cef2be0db00e7dc2df9bb924689707824bbb17f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Jul 2018 23:14:04 +0300 Subject: [PATCH 19/24] Implement /portal/{mxid}/create --- mautrix_telegram/__main__.py | 2 +- mautrix_telegram/commands/portal.py | 26 ++-- mautrix_telegram/user.py | 3 + mautrix_telegram/web/common/auth_api.py | 10 +- mautrix_telegram/web/provisioning/__init__.py | 85 +++++++++++- mautrix_telegram/web/provisioning/spec.yaml | 126 ++++++++++++++---- 6 files changed, 205 insertions(+), 47 deletions(-) diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 1969437b..bd31ab9d 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -93,7 +93,7 @@ if config["appservice.public.enabled"]: appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public_website.app) if config["appservice.provisioning.enabled"]: - provisioning_api = ProvisioningAPI(config, loop) + provisioning_api = ProvisioningAPI(config, appserv, loop) appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning", provisioning_api.app) diff --git a/mautrix_telegram/commands/portal.py b/mautrix_telegram/commands/portal.py index c87ade32..e9e29547 100644 --- a/mautrix_telegram/commands/portal.py +++ b/mautrix_telegram/commands/portal.py @@ -19,10 +19,11 @@ import asyncio from telethon.errors import * from telethon.tl.types import ChatForbidden, ChannelForbidden -from mautrix_appservice import MatrixRequestError +from mautrix_appservice import MatrixRequestError, IntentAPI from .. import portal as po, user as u -from . import command_handler, CommandEvent, SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT +from . import (command_handler, CommandEvent, + SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT) @command_handler(needs_admin=True, needs_auth=False, name="set-pl", @@ -65,7 +66,7 @@ async def invite_link(evt: CommandEvent): return await evt.reply("You don't have the permission to create an invite link.") -async def _has_access_to(room: str, intent, sender: u.User, event: str, default: int = 50): +async def user_has_power_level(room: str, intent, sender: u.User, event: str, default: int = 50): if sender.is_admin: return True # Make sure the state store contains the power levels. @@ -87,7 +88,7 @@ async def _get_portal_and_check_permission(evt: CommandEvent, permission: str, that_this = "This" if room_id == evt.room_id else "That" return await evt.reply(f"{that_this} is not a portal room."), False - if not await _has_access_to(portal.mxid, evt.az.intent, evt.sender, permission): + if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, permission): action = action or f"{permission.replace('_', ' ')}s" return await evt.reply(f"You do not have the permissions to {action} that portal."), False return portal, True @@ -166,8 +167,8 @@ async def bridge(evt: CommandEvent): if portal: return await evt.reply(f"{that_this} room is already a portal room.") - if not await _has_access_to(room_id, evt.az.intent, evt.sender, "bridge"): - return await evt.reply("You do not have the permissions to bridge that room.") + if not await user_has_power_level(room_id, evt.az.intent, evt.sender, "bridge"): + return await evt.reply(f"You do not have the permissions to bridge {that_this} room.") # The /id bot command provides the prefixed ID, so we assume tgid = evt.args[0] @@ -192,7 +193,7 @@ async def bridge(evt: CommandEvent): has_portal_message = ( "That Telegram chat already has a portal at " f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). ") - if not await _has_access_to(portal.mxid, evt.az.intent, evt.sender, "unbridge"): + 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.") @@ -291,7 +292,7 @@ async def confirm_bridge(evt: CommandEvent): direct = False portal.mxid = bridge_to_mxid - portal.title, portal.about, levels = await _get_initial_state(evt) + portal.title, portal.about, levels = await get_initial_state(evt.az.intent, evt.room_id) portal.photo_id = "" portal.save() @@ -301,8 +302,8 @@ async def confirm_bridge(evt: CommandEvent): return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.") -async def _get_initial_state(evt: CommandEvent): - state = await evt.az.intent.get_room_state(evt.room_id) +async def get_initial_state(intent: IntentAPI, room_id: str): + state = await intent.get_room_state(room_id) title = None about = None levels = None @@ -336,7 +337,10 @@ async def create(evt: CommandEvent): if po.Portal.get_by_mxid(evt.room_id): return await evt.reply("This is already a portal room.") - title, about, levels = await _get_initial_state(evt) + if not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"): + return await evt.reply("You do not have the permissions to bridge this room.") + + title, about, levels = await get_initial_state(evt.az.intent, evt.room_id) if not title: return await evt.reply("Please set a title before creating a Telegram chat.") diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 1163e503..ea0b92e3 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -310,6 +310,9 @@ class User(AbstractUser): @classmethod def get_by_mxid(cls, mxid, create=True): + if not mxid: + raise ValueError("Matrix ID can't be empty") + try: return cls.by_mxid[mxid] except KeyError: diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py index 2ea50bca..14d29963 100644 --- a/mautrix_telegram/web/common/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -21,8 +21,8 @@ import logging from telethon.errors import * -from mautrix_telegram.commands.auth import enter_password -from mautrix_telegram.util import format_duration +from ...commands.auth import enter_password +from ...util import format_duration class AuthAPI(abc.ABC): @@ -70,7 +70,7 @@ class AuthAPI(abc.ABC): except Exception: self.log.exception("Error requesting phone code") return self.get_login_response(mxid=user.mxid, state="request", status=500, - errcode="exception", + errcode="unknown_error", error="Internal server error while requesting code.") async def post_login_token(self, user, token): @@ -124,7 +124,7 @@ class AuthAPI(abc.ABC): except Exception: self.log.exception("Error sending phone code") return self.get_login_response(mxid=user.mxid, state="code", status=500, - errcode="exception", + errcode="unknown_error", error="Internal server error while sending code.") async def post_login_password(self, user, password): @@ -146,5 +146,5 @@ class AuthAPI(abc.ABC): except Exception: self.log.exception("Error sending password") return self.get_login_response(mxid=user.mxid, state="password", status=500, - errcode="exception", + errcode="unknown_error", error="Internal server error while sending password.") diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index e3fa8347..dc640c93 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -16,32 +16,37 @@ # along with this program. If not, see . from aiohttp import web from typing import Tuple, Optional, Callable, Awaitable +import asyncio import logging import json from telethon.utils import get_peer_id, resolve_id +from mautrix_appservice import AppService, MatrixRequestError, IntentError from ...user import User from ...portal import Portal +from ...commands.portal import user_has_power_level, get_initial_state +from ...config import Config from ..common import AuthAPI class ProvisioningAPI(AuthAPI): log = logging.getLogger("mau.web.provisioning") - def __init__(self, config, loop): + def __init__(self, config: Config, az: AppService, loop: asyncio.AbstractEventLoop): super().__init__(loop) self.secret = config["appservice.provisioning.shared_secret"] + self.az = az self.app = web.Application(loop=loop, middlewares=[self.error_middleware]) portal_prefix = "/portal/{mxid:![^/]+}" self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal_by_mxid) self.app.router.add_route("GET", "/portal/{tgid:-[0-9]+}", self.get_portal_by_tgid) - # self.app.router.add_route("POST", portal_prefix + "/connect/{chat_id:[0-9]+}", - # self.connect_chat) - # self.app.router.add_route("POST", f"{portal_prefix}/create", self.create_chat) - # self.app.router.add_route("POST", f"{portal_prefix}/disconnect", self.disconnect_chat) + self.app.router.add_route("POST", portal_prefix + "/connect/{chat_id:[0-9]+}", + self.connect_chat) + self.app.router.add_route("POST", f"{portal_prefix}/create", self.create_chat) + self.app.router.add_route("POST", f"{portal_prefix}/disconnect", self.disconnect_chat) user_prefix = "/user/{mxid:@[^:]*:[^/]+}" self.app.router.add_route("GET", f"{user_prefix}", self.get_user_info) @@ -88,6 +93,69 @@ class ProvisioningAPI(AuthAPI): "megagroup": portal.megagroup, }) + async def connect_chat(self, request: web.Request) -> web.Response: + return web.Response(status=501) + + async def create_chat(self, request: web.Request) -> web.Response: + data = await self.get_data(request) + if not data: + return self.get_error_response(400, "json_invalid", "Invalid JSON.") + + room_id = request.match_info["mxid"] + if Portal.get_by_mxid(room_id): + return self.get_error_response(409, "room_already_bridged", + "Room is already bridged to another Telegram chat.") + + user, err = await self.get_user(request.query.get("user_id", None), expect_logged_in=None, + require_puppeting=False) + if err is not None: + return err + elif not await user.is_logged_in() or user.is_bot: + return self.get_error_response(403, "not_logged_in_real_account", + "You are not logged in with a real account.") + elif not await user_has_power_level(room_id, self.az.intent, user, "bridge"): + return self.get_error_response(403, "not_enough_permissions", + "You do not have the permissions to bridge that room.") + + try: + title, about, _ = await get_initial_state(self.az.intent, room_id) + except (MatrixRequestError, IntentError): + return self.get_error_response(403, "bot_not_in_room", + "The bridge bot is not in the given room.") + + about = data.get("about", about) + + title = data.get("title", title) + if len(title) == 0: + return self.get_error_response(400, "body_value_invalid", "Title can not be empty.") + + type = data.get("type", "") + if type not in ("group", "chat", "supergroup", "channel"): + return self.get_error_response(400, "body_value_invalid", + "Given chat type is not valid.") + + supergroup = type == "supergroup" + type = { + "supergroup": "channel", + "channel": "channel", + "chat": "chat", + "group": "chat", + }[type] + + portal = Portal(tgid=None, mxid=room_id, title=title, about=about, peer_type=type) + try: + await portal.create_telegram_chat(user, supergroup=supergroup) + except ValueError as e: + portal.delete() + return self.get_error_response(500, "unknown_error", e.args[0]) + + return web.json_response({ + "chat_id": portal.tgid, + }) + + async def disconnect_chat(self, request: web.Request) -> web.Response: + return web.Response(status=501) + async def get_user_info(self, request: web.Request) -> web.Response: data, user, err = await self.get_user_request_info(request, expect_logged_in=None, require_puppeting=False) @@ -187,10 +255,11 @@ class ProvisioningAPI(AuthAPI): } else: resp = { - "state": state, "error": error, "errcode": errcode, } + if state: + resp["state"] = state return web.json_response(resp, status=status) def check_authorization(self, request: web.Request) -> bool: @@ -206,6 +275,10 @@ class ProvisioningAPI(AuthAPI): async def get_user(self, mxid: str, expect_logged_in: Optional[bool] = False, require_puppeting: bool = True, ) -> Tuple[Optional[User], Optional[web.Response]]: + if not mxid: + return None, self.get_login_response(error="User ID not given.", + errcode="mxid_empty", status=400) + user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) if require_puppeting and not user.puppet_whitelisted: return user, self.get_login_response(error="You are not whitelisted.", diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index 775821b9..b107d47c 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -35,7 +35,7 @@ paths: schema: $ref: "#/definitions/PortalInfo" 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" 404: description: Unknown portal schema: @@ -127,9 +127,16 @@ paths: enum: - delete - unbridge + - name: user_id + in: query + description: Optional Matrix user ID to check if the user has permissions to do the bridging. + required: false + type: string responses: 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" + 401: + $ref: "#/responses/PermissionError" 409: description: Matrix room or Telegram chat is already bridged schema: @@ -152,8 +159,33 @@ paths: summary: Create a new Telegram chat for the given room tags: [Bridging] responses: + 200: + description: Telegram chat created + schema: + type: object + properties: + chat_id: + type: integer 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" + 401: + $ref: "#/responses/PermissionError" + 403: + description: "Given user isn't logged in with a real account or doesn't have permission to bridge the room, or the bridge bot is not in the room" + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - not_logged_in_real_account + - not_enough_permissions + - bot_not_in_room + error: + $ref: "#/definitions/HumanReadableError" 409: description: Room is already bridged schema: @@ -174,6 +206,34 @@ paths: description: The Matrix ID of the room whose bridging status to get required: true type: string + - name: body + in: body + required: true + schema: + type: object + required: [type] + properties: + type: + description: The type of chat to create + type: string + example: supergroup + enum: + - chat + - supergroup + - channel + title: + description: Title for the new chat + type: string + example: Mautrix-Telegram Bridge + about: + description: About text for the new chat + type: string + example: Discussion about mautrix-telegram + - name: user_id + in: query + description: Matrix user to create the chat as. + required: true + type: string /portal/{room_id}/disconnect: post: operationId: disconnect_portal @@ -183,7 +243,9 @@ paths: 202: description: Room unbridging initiated 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" + 401: + $ref: "#/responses/PermissionError" 404: description: Unknown portal schema: @@ -204,6 +266,11 @@ paths: description: The Matrix ID of the room whose bridging status to get required: true type: string + - name: user_id + in: query + description: Optional Matrix user ID to check if the user has permissions to do the bridging. + required: false + type: string /user/{user_id}: get: @@ -216,7 +283,7 @@ paths: schema: $ref: "#/definitions/UserInfo" 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" 403: $ref: "#/responses/NotWhitelistedError" 500: @@ -238,7 +305,7 @@ paths: schema: $ref: "#/definitions/UserChats" 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" 403: description: User is not logged in or not whitelisted schema: @@ -274,7 +341,7 @@ paths: schema: $ref: "#/definitions/AuthSuccess" 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" 401: description: Invalid or expired bot token or invalid shared secret schema: @@ -325,7 +392,7 @@ paths: schema: $ref: "#/definitions/AuthSuccess" 400: - description: Invalid phone number or JSON or missing Matrix ID + description: Invalid phone number or JSON schema: type: object title: Error @@ -337,7 +404,6 @@ paths: example: machine_readable_error enum: - phone_number_invalid - - mxid_empty - json_invalid error: $ref: "#/definitions/HumanReadableError" @@ -436,7 +502,7 @@ paths: schema: $ref: "#/definitions/AuthSuccess" 400: - $ref: "#/responses/MissingMXIDError" + $ref: "#/responses/BadRequest" 401: description: Invalid phone code or shared secret schema: @@ -500,7 +566,7 @@ paths: schema: $ref: "#/definitions/AuthSuccess" 400: - description: Missing password or Matrix ID or invalid JSON + description: Missing password or invalid JSON schema: type: object title: Error @@ -512,7 +578,6 @@ paths: example: _empty enum: - password_empty - - mxid_empty - json_invalid error: $ref: "#/definitions/HumanReadableError" @@ -582,8 +647,8 @@ responses: username: type: string description: The Telegram username the user is logged in as. - MissingMXIDError: - description: Missing Matrix ID or invalid JSON. + BadRequest: + description: Invalid JSON. schema: type: object title: Error @@ -593,8 +658,10 @@ responses: title: Error code description: A machine-readable error code enum: - - mxid_empty - json_invalid + - mxid_empty + - body_value_missing + - body_value_invalid error: $ref: "#/definitions/HumanReadableError" UnknownError: @@ -608,23 +675,30 @@ responses: title: Error code description: A machine-readable error code enum: - - exception + - unknown_error + - unhandled_error error: type: string title: Error description: A human-readable description of the error example: Internal server error while . + PermissionError: + description: The given Matrix user doesn't have the permissions to do that. + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + example: not_enough_permissions enum: - - Internal server error while requesting code. - - Internal server error while sending code. - - Internal server error while sending password. - - Internal server error while sending token. + - not_enough_permissions + error: + $ref: "#/definitions/HumanReadableError" definitions: - HumanReadableError: - type: string - description: A human-readable description of the error - example: A human-readable description of the error UserInfo: type: object properties: @@ -713,6 +787,10 @@ definitions: type: string description: The Telegram username the user is logged in as. Only applicable if state=logged-in + HumanReadableError: + type: string + description: A human-readable description of the error + example: A human-readable description of the error security: - Bearer: [] From cea52102902480534610fc277a12c13df8bdc523 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 14 Jul 2018 23:15:28 +0300 Subject: [PATCH 20/24] Add /v1 prefix to provisioning API by default --- example-config.yaml | 2 +- mautrix_telegram/web/provisioning/spec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index eb6c57d8..69c832f3 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -39,7 +39,7 @@ appservice: # Whether or not the provisioning API should be enabled. enabled: true # The prefix to use in the provisioning API endpoints. - prefix: /_matrix/provision + prefix: /_matrix/provision/v1 # The shared secret to authorize users of the API. # If you leave the default token, a random token will be generated and saved at startup. shared_secret: "Very secret shared secret" diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index b107d47c..fdd5a5b9 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -12,7 +12,7 @@ externalDocs: description: Provisioning API wiki page on GitHub url: https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API -basePath: /_matrix/provision +basePath: /_matrix/provision/v1 schemes: [https] consumes: [application/json] From a46cc7a78895ced8b9519933c18f0f97f7ed1d3c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 15 Jul 2018 12:38:24 +0300 Subject: [PATCH 21/24] Add logout endpoint --- mautrix_telegram/web/provisioning/__init__.py | 20 +++++++++---- mautrix_telegram/web/provisioning/spec.yaml | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index dc640c93..f853d041 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -52,10 +52,11 @@ class ProvisioningAPI(AuthAPI): self.app.router.add_route("GET", f"{user_prefix}", self.get_user_info) self.app.router.add_route("GET", f"{user_prefix}/chats", self.get_chats) - self.app.router.add_route("POST", f"{user_prefix}/send_bot_token", self.send_bot_token) - self.app.router.add_route("POST", f"{user_prefix}/request_code", self.request_code) - self.app.router.add_route("POST", f"{user_prefix}/send_code", self.send_code) - self.app.router.add_route("POST", f"{user_prefix}/send_password", self.send_password) + self.app.router.add_route("POST", f"{user_prefix}/logout", self.logout) + self.app.router.add_route("POST", f"{user_prefix}/login/bot_token", self.send_bot_token) + self.app.router.add_route("POST", f"{user_prefix}/login/request_code", self.request_code) + self.app.router.add_route("POST", f"{user_prefix}/login/send_code", self.send_code) + self.app.router.add_route("POST", f"{user_prefix}/login/send_password", self.send_password) async def get_portal_by_mxid(self, request: web.Request) -> web.Response: mxid = request.match_info["mxid"] @@ -221,6 +222,14 @@ class ProvisioningAPI(AuthAPI): return err return await self.post_login_password(user, data.get("password", "")) + async def logout(self, request: web.Request) -> web.Response: + _, user, err = await self.get_user_request_info(request, expect_logged_in=True, + require_puppeting=False, + want_data=False) + if err is not None: + return err + await user.log_out() + @staticmethod async def error_middleware(_, handler) -> Callable[[web.Request], Awaitable[web.Response]]: async def middleware_handler(request: web.Request) -> web.Response: @@ -297,6 +306,7 @@ class ProvisioningAPI(AuthAPI): async def get_user_request_info(self, request: web.Request, expect_logged_in: Optional[bool] = False, require_puppeting: bool = False, + want_data: bool = True, ) -> (Tuple[Optional[dict], Optional[User], Optional[web.Response]]): @@ -307,7 +317,7 @@ class ProvisioningAPI(AuthAPI): status=401) data = None - if request.method == "POST" or request.method == "PUT": + if want_data and (request.method == "POST" or request.method == "PUT"): data = await self.get_data(request) if not data: return None, None, self.get_login_response(error="Invalid JSON.", diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index fdd5a5b9..94457ce4 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -619,6 +619,36 @@ paths: description: The two-factor auth password format: password example: hunter2 + /user/{user_id}/logout: + post: + operationId: logout + summary: Log out + tags: [Authentication] + responses: + 200: + description: Logout successful + 403: + description: User was not logged in + schema: + type: object + title: Error + properties: + errcode: + type: string + title: Error code + description: A machine-readable error code + enum: + - not_logged_in + error: + $ref: "#/definitions/HumanReadableError" + 500: + $ref: "#/responses/UnknownError" + parameters: + - name: user_id + in: path + description: The Matrix ID of the user who to log out as + required: true + type: string responses: NotWhitelistedError: From c2879408ccd49f77faf2e9a94cc64634ee27481c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 15 Jul 2018 14:51:56 +0300 Subject: [PATCH 22/24] Make bridging permission checks consistent --- mautrix_telegram/commands/portal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mautrix_telegram/commands/portal.py b/mautrix_telegram/commands/portal.py index e9e29547..38998224 100644 --- a/mautrix_telegram/commands/portal.py +++ b/mautrix_telegram/commands/portal.py @@ -117,7 +117,7 @@ def _get_portal_murder_function(action: str, room_id: str, function: Callable, c "Only works for group chats; to delete a private chat portal, simply " "leave the room.") async def delete_portal(evt: CommandEvent): - portal, ok = await _get_portal_and_check_permission(evt, "delete_portal") + portal, ok = await _get_portal_and_check_permission(evt, "unbridge") if not ok: return @@ -138,7 +138,7 @@ async def delete_portal(evt: CommandEvent): help_section=SECTION_PORTAL_MANAGEMENT, help_text="Remove puppets from the current portal room and forget the portal.") async def unbridge(evt: CommandEvent): - portal, ok = await _get_portal_and_check_permission(evt, "unbridge_room") + portal, ok = await _get_portal_and_check_permission(evt, "unbridge") if not ok: return From c55967c9f0ef98b75bc8e526dfbdd5b60f91f5fa Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 15 Jul 2018 15:19:37 +0300 Subject: [PATCH 23/24] Implement disconnecting portals via provisioning API --- mautrix_telegram/web/common/auth_api.py | 2 +- mautrix_telegram/web/provisioning/__init__.py | 72 ++++++++++++++++--- mautrix_telegram/web/provisioning/spec.yaml | 12 ++++ 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py index 14d29963..70b66136 100644 --- a/mautrix_telegram/web/common/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -29,7 +29,7 @@ class AuthAPI(abc.ABC): log = logging.getLogger("mau.web.auth") def __init__(self, loop): - self.loop = loop + self.loop = loop # type: asyncio.AbstractEventLoop @abstractmethod def get_login_response(self, status=200, state="", username="", mxid="", message="", error="", diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index f853d041..63bb208d 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -59,6 +59,10 @@ class ProvisioningAPI(AuthAPI): self.app.router.add_route("POST", f"{user_prefix}/login/send_password", self.send_password) async def get_portal_by_mxid(self, request: web.Request) -> web.Response: + err = self.check_authorization(request) + if err is not None: + return err + mxid = request.match_info["mxid"] portal = Portal.get_by_mxid(mxid) if not portal: @@ -75,6 +79,10 @@ class ProvisioningAPI(AuthAPI): }) async def get_portal_by_tgid(self, request: web.Request) -> web.Response: + err = self.check_authorization(request) + if err is not None: + return err + try: tgid, _ = resolve_id(int(request.match_info["tgid"])) except ValueError: @@ -95,9 +103,19 @@ class ProvisioningAPI(AuthAPI): }) async def connect_chat(self, request: web.Request) -> web.Response: - return web.Response(status=501) + err = self.check_authorization(request) + if err is not None: + return err + + return self.get_error_response(501, "not_implemented", + "Connecting existing Matrix rooms to existing Telegram " + "chats via the provisioning API is not yet implemented.") async def create_chat(self, request: web.Request) -> web.Response: + err = self.check_authorization(request) + if err is not None: + return err + data = await self.get_data(request) if not data: return self.get_error_response(400, "json_invalid", "Invalid JSON.") @@ -155,7 +173,36 @@ class ProvisioningAPI(AuthAPI): }) async def disconnect_chat(self, request: web.Request) -> web.Response: - return web.Response(status=501) + err = self.check_authorization(request) + if err is not None: + return err + + portal = Portal.get_by_mxid(request.match_info["mxid"]) + if not portal or not portal.tgid: + return self.get_error_response(404, "portal_not_found", + "Room is not a portal.") + + user, err = await self.get_user(request.query.get("user_id", None), expect_logged_in=None, + require_puppeting=False, require_user=False) + if err is not None: + return err + elif user and not await user_has_power_level(portal.mxid, self.az.intent, user, "unbridge"): + return self.get_error_response(403, "not_enough_permissions", + "You do not have the permissions to unbridge that room.") + + delete = request.query.get("delete", "").lower() in ("true", "t", "1", "yes", "y") + sync = request.query.get("delete", "").lower() in ("true", "t", "1", "yes", "y") + + coro = portal.cleanup_and_delete() if delete else portal.unbridge() + if sync: + try: + await coro + except Exception: + self.log.exception("Failed to disconnect chat") + return self.get_error_response(500, "exception", "Failed to disconnect chat") + else: + asyncio.ensure_future(coro, loop=self.loop) + return web.json_response({}, status=200 if sync else 202) async def get_user_info(self, request: web.Request) -> web.Response: data, user, err = await self.get_user_request_info(request, expect_logged_in=None, @@ -271,8 +318,13 @@ class ProvisioningAPI(AuthAPI): resp["state"] = state return web.json_response(resp, status=status) - def check_authorization(self, request: web.Request) -> bool: - return request.headers.get("Authorization", "") == f"Bearer {self.secret}" + def check_authorization(self, request: web.Request) -> Optional[web.Response]: + auth = request.headers.get("Authorization", "") + if auth != f"Bearer {self.secret}": + return self.get_error_response(error="Shared secret is not valid.", + errcode="shared_secret_invalid", + status=401) + return None @staticmethod async def get_data(request: web.Request) -> Optional[dict]: @@ -282,9 +334,11 @@ class ProvisioningAPI(AuthAPI): return None async def get_user(self, mxid: str, expect_logged_in: Optional[bool] = False, - require_puppeting: bool = True, + require_puppeting: bool = True, require_user: bool = True ) -> Tuple[Optional[User], Optional[web.Response]]: if not mxid: + if not require_user: + return None, None return None, self.get_login_response(error="User ID not given.", errcode="mxid_empty", status=400) @@ -310,11 +364,9 @@ class ProvisioningAPI(AuthAPI): ) -> (Tuple[Optional[dict], Optional[User], Optional[web.Response]]): - auth = request.headers.get("Authorization", "") - if auth != f"Bearer {self.secret}": - return None, None, self.get_login_response(error="Shared secret is not valid.", - errcode="shared_secret_invalid", - status=401) + err = self.check_authorization(request) + if err is not None: + return err data = None if want_data and (request.method == "POST" or request.method == "PUT"): diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index 94457ce4..c44becdc 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -271,6 +271,18 @@ paths: description: Optional Matrix user ID to check if the user has permissions to do the bridging. required: false type: string + - name: delete + in: query + description: Whether or not to delete the room completely (kick all users instead of just Telegram puppets) + required: false + type: boolean + default: false + - name: sync + in: query + description: Whether or not to wait for the unbridging to be completed before responding. **Could cause timeouts in large rooms** + required: false + type: boolean + default: false /user/{user_id}: get: From 4d63cd75d42eff61eec3852d3192d133225bfd4a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 15 Jul 2018 15:32:37 +0300 Subject: [PATCH 24/24] Update spec metadata --- mautrix_telegram/web/provisioning/spec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index c44becdc..4c1c44c4 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -1,9 +1,9 @@ swagger: "2.0" info: - title: mautrix-telegram provisioning + title: Mautrix-Telegram provisioning version: 0.3.0 - description: The provisioning API for mautrix-telegram. + description: The provisioning API for Mautrix-Telegram, the Matrix-Telegram puppeting/relaybot bridge. license: name: AGPLv3 url: https://github.com/tulir/mautrix-telegram/blob/master/LICENSE