# -*- coding: future_fstrings -*- # mautrix-telegram - A Matrix-Telegram puppeting bridge # Copyright (C) 2019 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import Dict, Optional, Tuple, Coroutine import asyncio from telethon.tl.types import ChatForbidden, ChannelForbidden from ...types import MatrixRoomID, TelegramID from ...util import ignore_coro from ... import portal as po from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS from .util import user_has_power_level, get_initial_state @command_handler(needs_auth=False, needs_puppeting=False, help_section=SECTION_CREATING_PORTALS, help_args="[_id_]", help_text="Bridge the current Matrix room to the Telegram chat with the given " "ID. The ID must be the prefixed version that you get with the `/id` " "command of the Telegram-side bot.") async def bridge(evt: CommandEvent) -> Dict: if len(evt.args) == 0: return await evt.reply("**Usage:** " "`$cmdprefix+sp bridge [Matrix room ID]`") room_id = MatrixRoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id that_this = "This" if room_id == evt.room_id else "That" portal = po.Portal.get_by_mxid(room_id) if portal: return await evt.reply(f"{that_this} room is already a portal 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_str = evt.args[0] if tgid_str.startswith("-100"): tgid = TelegramID(int(tgid_str[4:])) peer_type = "channel" elif tgid_str.startswith("-"): tgid = TelegramID(-int(tgid_str)) peer_type = "chat" else: return await evt.reply("That doesn't seem like a prefixed Telegram chat ID.\n\n" "If you did not get the ID using the `/id` bot command, please " "prefix channel IDs with `-100` and normal group IDs with `-`.\n\n" "Bridging private chats to existing rooms is not allowed.") portal = po.Portal.get_by_tgid(tgid, peer_type=peer_type) if not portal.allow_bridging(): return await evt.reply("This bridge doesn't allow bridging that Telegram chat.\n" "If you're the bridge admin, try " "`$cmdprefix+sp filter whitelist ` first.") if portal.mxid: has_portal_message = ( "That Telegram chat already has a portal at " f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). ") 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.") evt.sender.command_status = { "next": confirm_bridge, "action": "Room bridging", "mxid": portal.mxid, "bridge_to_mxid": room_id, "tgid": portal.tgid, "peer_type": portal.peer_type, } return await evt.reply(f"{has_portal_message}" "However, you have the permissions to unbridge that room.\n\n" "To delete that portal completely and continue bridging, use " "`$cmdprefix+sp delete-and-continue`. To unbridge the portal " "without kicking Matrix users, use `$cmdprefix+sp unbridge-and-" "continue`. To cancel, use `$cmdprefix+sp cancel`") evt.sender.command_status = { "next": confirm_bridge, "action": "Room bridging", "bridge_to_mxid": room_id, "tgid": portal.tgid, "peer_type": portal.peer_type, } return await evt.reply("That Telegram chat has no existing portal. To confirm bridging the " "chat to this room, use `$cmdprefix+sp continue`") async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Portal" ) -> Tuple[bool, Optional[Coroutine[None, None, None]]]: if not portal.mxid: await evt.reply("The portal seems to have lost its Matrix room between you" "calling `$cmdprefix+sp bridge` and this command.\n\n" "Continuing without touching previous Matrix room...") return True, None elif evt.args[0] == "delete-and-continue": return True, portal.cleanup_room(portal.main_intent, portal.mxid, message="Portal deleted (moving to another room)") elif evt.args[0] == "unbridge-and-continue": return True, portal.cleanup_room(portal.main_intent, portal.mxid, message="Room unbridged (portal moving to another room)", puppets_only=True) else: await evt.reply( "The chat you were trying to bridge already has a Matrix portal room.\n\n" "Please use `$cmdprefix+sp delete-and-continue` or `$cmdprefix+sp unbridge-and-" "continue` to either delete or unbridge the existing room (respectively) and " "continue with the bridging.\n\n" "If you changed your mind, use `$cmdprefix+sp cancel` to cancel.") return False, None async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]: status = evt.sender.command_status try: portal = po.Portal.get_by_tgid(status["tgid"], peer_type=status["peer_type"]) bridge_to_mxid = status["bridge_to_mxid"] except KeyError: evt.sender.command_status = None return await evt.reply("Fatal error: tgid or peer_type missing from command_status. " "This shouldn't happen unless you're messing with the command " "handler code.") if "mxid" in status: ok, coro = await cleanup_old_portal_while_bridging(evt, portal) if not ok: return None elif coro: ignore_coro(asyncio.ensure_future(coro, loop=evt.loop)) await evt.reply("Cleaning up previous portal room...") elif portal.mxid: evt.sender.command_status = None return await evt.reply("The portal seems to have created a Matrix room between you " "calling `$cmdprefix+sp bridge` and this command.\n\n" "Please start over by calling the bridge command again.") elif evt.args[0] != "continue": return await evt.reply("Please use `$cmdprefix+sp continue` to confirm the bridging or " "`$cmdprefix+sp cancel` to cancel.") evt.sender.command_status = None is_logged_in = await evt.sender.is_logged_in() user = evt.sender if is_logged_in else evt.tgbot try: entity = await user.client.get_entity(portal.peer) except Exception: evt.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer) if is_logged_in: return await evt.reply("Failed to get info of telegram chat. " "You are logged in, are you in that chat?") else: return await evt.reply("Failed to get info of telegram chat. " "You're not logged in, is the relay bot in the chat?") if isinstance(entity, (ChatForbidden, ChannelForbidden)): if is_logged_in: return await evt.reply("You don't seem to be in that chat.") else: return await evt.reply("The bot doesn't seem to be in that chat.") direct = False portal.mxid = bridge_to_mxid portal.title, portal.about, levels = await get_initial_state(evt.az.intent, evt.room_id) portal.photo_id = "" portal.save() ignore_coro(asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels), loop=evt.loop)) return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")