diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index ed566c6d..b1768a8f 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -88,20 +88,19 @@ appserv = AppService(config["homeserver.address"], config["homeserver.domain"], verify_ssl=config["homeserver.verify_ssl"], state_store=state_store, real_user_content_key="net.maunium.telegram.puppet") -public_website = None # type: Optional[PublicBridgeWebsite] -provisioning_api = None # type: Optional[ProvisioningAPI] +context = Context(appserv, db_session, config, loop, session_container) if config["appservice.public.enabled"]: public_website = PublicBridgeWebsite(loop) appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public_website.app) + context.public_website = public_website if config["appservice.provisioning.enabled"]: - provisioning_api = ProvisioningAPI(config, appserv, loop) + provisioning_api = ProvisioningAPI(context) appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning", provisioning_api.app) + context.provisioning_api = provisioning_api -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) diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py index 76f75ded..de257477 100644 --- a/mautrix_telegram/context.py +++ b/mautrix_telegram/context.py @@ -14,7 +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 TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: import asyncio @@ -32,18 +32,16 @@ if TYPE_CHECKING: class Context: def __init__(self, az: "AppService", db: "scoped_session", config: "Config", - loop: "asyncio.AbstractEventLoop", bot: "Bot", mx: "MatrixHandler", - session_container: "AlchemySessionContainer", - public_website: "PublicBridgeWebsite", provisioning_api: "ProvisioningAPI"): + loop: "asyncio.AbstractEventLoop", session_container: "AlchemySessionContainer"): 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.bot = None # type: Optional[Bot] + self.mx = None # type: MatrixHandler self.session_container = session_container # type: AlchemySessionContainer - self.public_website = public_website # type: PublicBridgeWebsite - self.provisioning_api = provisioning_api # type: ProvisioningAPI + self.public_website = None # type: PublicBridgeWebsite + self.provisioning_api = None # type: ProvisioningAPI def __iter__(self): yield self.az diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 731a91ff..04aa499a 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -15,30 +15,34 @@ # 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 +from typing import Tuple, Optional, Callable, Awaitable, TYPE_CHECKING import asyncio import logging import json from telethon.utils import get_peer_id, resolve_id +from telethon.tl.types import ChatForbidden, ChannelForbidden, TypeChat 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 +if TYPE_CHECKING: + from ...context import Context + class ProvisioningAPI(AuthAPI): log = logging.getLogger("mau.web.provisioning") - def __init__(self, config: Config, az: AppService, loop: asyncio.AbstractEventLoop): - super().__init__(loop) - self.secret = config["appservice.provisioning.shared_secret"] - self.az = az + def __init__(self, context: "Context"): + super().__init__(context.loop) + self.secret = context.config["appservice.provisioning.shared_secret"] + self.az = context.az # type: AppService + self.context = context # type: Context - self.app = web.Application(loop=loop, middlewares=[self.error_middleware]) + self.app = web.Application(loop=context.loop, middlewares=[self.error_middleware]) portal_prefix = "/portal/{mxid:![^/]+}" self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal_by_mxid) @@ -107,9 +111,79 @@ class ProvisioningAPI(AuthAPI): 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.") + 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.") + + chat_id = request.match_info["chat_id"] + if chat_id.startswith("-100"): + tgid = int(chat_id[4:]) + peer_type = "channel" + elif chat_id.startswith("-"): + tgid = -int(chat_id) + peer_type = "chat" + else: + return self.get_error_response(400, "tgid_invalid", "Invalid Telegram chat ID.") + + 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 user and 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.") + + portal = Portal.get_by_tgid(tgid, peer_type=peer_type) + if portal.mxid == room_id: + return self.get_error_response(200, "bridge_exists", + "Telegram chat is already bridged to that Matrix room.") + elif portal.mxid: + force = request.query.get("force", None) + if force in ("delete", "unbridge"): + delete = force == "delete" + await portal.cleanup_room(portal.main_intent, portal.mxid, puppets_only=not delete, + message=("Portal deleted (moving to another room)" + if delete + else "Room unbridged (portal moving to another " + "room)")) + else: + return self.get_error_response(409, "chat_already_bridged", + "Telegram chat is already bridged to another " + "Matrix room.") + + is_logged_in = user is not None and await user.is_logged_in() + user = user if is_logged_in else self.context.bot + if not user: + return self.get_login_response(status=403, errcode="not_logged_in", + error="You are not logged in and there is no relay bot.") + + entity = None # type: Optional[TypeChat] + try: + entity = await user.client.get_entity(portal.peer) + except Exception: + self.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer) + + if not entity or isinstance(entity, (ChatForbidden, ChannelForbidden)): + if is_logged_in: + return self.get_error_response(403, "user_not_in_chat", + "Failed to get info of Telegram chat. " + "Are you in the chat?") + return self.get_error_response(403, "bot_not_in_chat", + "Failed to get info of Telegram chat. " + "Is the relay bot in the chat?") + + direct = False + + portal.mxid = room_id + portal.title, portal.about, levels = await get_initial_state(self.az.intent, room_id) + portal.photo_id = "" + portal.save() + + asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels), + loop=self.loop) + + return web.Response(status=202, body="{}") async def create_chat(self, request: web.Request) -> web.Response: err = self.check_authorization(request) @@ -170,7 +244,7 @@ class ProvisioningAPI(AuthAPI): return web.json_response({ "chat_id": portal.tgid, - }) + }, status=201) async def disconnect_chat(self, request: web.Request) -> web.Response: err = self.check_authorization(request) diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml index 4c1c44c4..ed734bd2 100644 --- a/mautrix_telegram/web/provisioning/spec.yaml +++ b/mautrix_telegram/web/provisioning/spec.yaml @@ -133,12 +133,31 @@ paths: required: false type: string responses: + 200: + description: Telegram chat was already bridged to given room. + 202: + description: Room bridging initiated 400: $ref: "#/responses/BadRequest" - 401: - $ref: "#/responses/PermissionError" + 403: + description: "Given user 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_enough_permissions + - bot_not_in_room + - bot_not_in_chat + - not_logged_in + error: + $ref: "#/definitions/HumanReadableError" 409: - description: Matrix room or Telegram chat is already bridged + description: Matrix room or Telegram chat is already bridged to another chat/room schema: type: object title: Error @@ -159,7 +178,7 @@ paths: summary: Create a new Telegram chat for the given room tags: [Bridging] responses: - 200: + 201: description: Telegram chat created schema: type: object @@ -168,8 +187,6 @@ paths: type: integer 400: $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: @@ -244,7 +261,7 @@ paths: description: Room unbridging initiated 400: $ref: "#/responses/BadRequest" - 401: + 403: $ref: "#/responses/PermissionError" 404: description: Unknown portal