Эх сурвалжийг харах

Complete bridge command, fix minor id command issue

Maximilian Gaedig 2 жил өмнө
parent
commit
b2321fbe5e

+ 163 - 48
mautrix_signal/commands/signal.py

@@ -18,6 +18,9 @@ from __future__ import annotations
 import base64
 import base64
 import json
 import json
 
 
+from typing import Awaitable
+import asyncio
+
 from mausignald.errors import UnknownIdentityKey, UnregisteredUserError
 from mausignald.errors import UnknownIdentityKey, UnregisteredUserError
 from mausignald.types import Address, GroupID, TrustLevel
 from mausignald.types import Address, GroupID, TrustLevel
 from mautrix.appservice import IntentAPI
 from mautrix.appservice import IntentAPI
@@ -25,7 +28,7 @@ from mautrix.bridge.commands import SECTION_ADMIN, HelpSection, command_handler
 from mautrix.types import ContentURI, EventID, EventType, PowerLevelStateEventContent, RoomID
 from mautrix.types import ContentURI, EventID, EventType, PowerLevelStateEventContent, RoomID
 
 
 from .. import portal as po, puppet as pu
 from .. import portal as po, puppet as pu
-from ..util import normalize_number
+from ..util import normalize_number, user_has_power_level
 from .auth import make_qr
 from .auth import make_qr
 from .typehint import CommandEvent
 from .typehint import CommandEvent
 
 
@@ -100,6 +103,8 @@ async def pm(evt: CommandEvent) -> None:
         )
         )
         await portal.main_intent.invite_user(portal.mxid, evt.sender.mxid)
         await portal.main_intent.invite_user(portal.mxid, evt.sender.mxid)
         return
         return
+
+        
     await portal.create_matrix_room(evt.sender, puppet.address)
     await portal.create_matrix_room(evt.sender, puppet.address)
     await evt.reply(f"Created a portal room with {_pill(puppet)} and invited you to it")
     await evt.reply(f"Created a portal room with {_pill(puppet)} and invited you to it")
 
 
@@ -337,17 +342,7 @@ async def create(evt: CommandEvent) -> EventID:
         receiver="",
         receiver="",
         avatar_url=avatar_url,
         avatar_url=avatar_url,
     )
     )
-    bot_pl = levels.get_user_level(evt.az.bot_mxid)
-    if bot_pl < levels.get_event_level(EventType.ROOM_POWER_LEVELS):
-        await evt.reply(missing_power_warning.format(bot_mxid=evt.az.bot_mxid))
-    elif bot_pl <= 50:
-        await evt.reply(low_power_warning.format(bot_mxid=evt.az.bot_mxid))
-    if levels.state_default < 50 and (
-        levels.events[EventType.ROOM_NAME] >= 50
-        or levels.events[EventType.ROOM_AVATAR] >= 50
-        or levels.events[EventType.ROOM_TOPIC] >= 50
-    ):
-        await evt.reply(meta_power_warning)
+    await warn_missing_power(levels, evt)
 
 
     await portal.create_signal_group(evt.sender, levels)
     await portal.create_signal_group(evt.sender, levels)
     await evt.reply(f"Signal chat created. ID: {portal.chat_id}")
     await evt.reply(f"Signal chat created. ID: {portal.chat_id}")
@@ -361,7 +356,7 @@ async def create(evt: CommandEvent) -> EventID:
 )
 )
 async def get_id(evt: CommandEvent) -> EventID:
 async def get_id(evt: CommandEvent) -> EventID:
     if evt.portal:
     if evt.portal:
-        await evt.reply(f"This room is bridged to Signal chat ID `{evt.portal.chat_id}`.")
+        return await evt.reply(f"This room is bridged to Signal chat ID `{evt.portal.chat_id}`.")
     await evt.reply("This is not a portal room.")
     await evt.reply("This is not a portal room.")
     
     
 
 
@@ -370,18 +365,25 @@ async def get_id(evt: CommandEvent) -> EventID:
     management_only=False,
     management_only=False,
     help_section=SECTION_SIGNAL,
     help_section=SECTION_SIGNAL,
     help_text="Bridge the current Matrix room to the Signal chat with the given ID.",
     help_text="Bridge the current Matrix room to the Signal chat with the given ID.",
-    help_args="<id>",
+    help_args="<Signal chat ID> [Matrix room ID]",
 )
 )
 async def bridge(evt: CommandEvent) -> EventID:
 async def bridge(evt: CommandEvent) -> EventID:
     if len(evt.args) == 0:
     if len(evt.args) == 0:
         return await evt.reply(
         return await evt.reply(
             "**Usage:** `$cmdprefix+sp bridge <Signal chat ID> [Matrix room ID]`"
             "**Usage:** `$cmdprefix+sp bridge <Signal chat ID> [Matrix room ID]`"
         )
         )
-    if evt.portal:
-        return await evt.reply("This is already a portal room.")
+    room_id = RoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id
+    that_this = "This" if room_id == evt.room_id else "That"
+
+    portal = await 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.")
     chat_id = None
     chat_id = None
     try: 
     try: 
-        chat_id= GroupID(evt.args[0])
+        chat_id = GroupID(evt.args[0])
     except ValueError:
     except ValueError:
         pass
         pass
     if not chat_id:
     if not chat_id:
@@ -389,43 +391,143 @@ async def bridge(evt: CommandEvent) -> EventID:
             "That doesn't seem like a Signal chat ID.\n\n"
             "That doesn't seem like a Signal chat ID.\n\n"
             "Bridging private chats to existing rooms is not allowed."
             "Bridging private chats to existing rooms is not allowed."
         )
         )
-    portal = await po.Portal.get_by_chat_id(
-        chat_id, create=True
-    )
-    title, about, levels, encrypted, avatar_url = await get_initial_state(
-        evt.az.intent, evt.room_id
-    )
+
+    portal = await po.Portal.get_by_chat_id(chat_id)
     if portal.mxid:
     if portal.mxid:
-        await evt.reply(
+        has_portal_message = (
             "That Signal chat already has a portal at "
             "That Signal chat already has a portal at "
-            f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). "
+            f"[{portal.mxid}](https://matrix.to/#/{portal.mxid}). "
         )
         )
-        await portal.main_intent.invite_user(portal.mxid, evt.sender.mxid)
-        return
-    portal = po.Portal(
-        chat_id=chat_id,
-        mxid=evt.room_id,
-        name=title,
-        topic=about or "",
-        encrypted=encrypted,
-        receiver="",
-        avatar_url=avatar_url,
+        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,
+            "chat_id": portal.chat_id,
+        }
+        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,
+        "chat_id": portal.chat_id,
+    }
+    return await evt.reply(
+        "That Signal chat has no existing portal. To confirm bridging the "
+        "chat to this room, use `$cmdprefix+sp continue`"
     )
     )
-    bot_pl = levels.get_user_level(evt.az.bot_mxid)
-    if bot_pl < levels.get_event_level(EventType.ROOM_POWER_LEVELS):
-        await evt.reply(missing_power_warning.format(bot_mxid=evt.az.bot_mxid))
-    elif bot_pl <= 50:
-        await evt.reply(low_power_warning.format(bot_mxid=evt.az.bot_mxid))
-    if levels.state_default < 50 and (
-        levels.events[EventType.ROOM_NAME] >= 50
-        or levels.events[EventType.ROOM_AVATAR] >= 50
-        or levels.events[EventType.ROOM_TOPIC] >= 50
-    ):
-        await evt.reply(meta_power_warning)
 
 
-    await portal.bridge_signal_group(evt.sender, levels)
-    await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
 
 
+async def cleanup_old_portal_while_bridging(
+    evt: CommandEvent, portal: po.Portal
+) -> tuple[bool, Awaitable[None] | None]:
+    if not portal.mxid:
+        await evt.reply(
+            "The portal seems to have lost its Matrix room between you"
+            "calling `$cmdprefix+sp bridge` and this command.\n\n"
+            "Continuing without touching previous Matrix room..."
+        )
+        return True, None
+    elif evt.args[0] == "delete-and-continue":
+        return True, portal.cleanup_portal("Portal deleted (moving to another room)", delete=False)
+    elif evt.args[0] == "unbridge-and-continue":
+        return True, portal.cleanup_portal(
+            "Room unbridged (portal moving to another room)", puppets_only=True, delete=False
+        )
+    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) -> EventID | None:
+    status = evt.sender.command_status
+    try:
+        portal = await po.Portal.get_by_chat_id(status["chat_id"])
+        bridge_to_mxid = status["bridge_to_mxid"]
+    except KeyError:
+        evt.sender.command_status = None
+        return await evt.reply(
+            "Fatal error: chat_id missing from command_status. "
+            "This shouldn't happen unless you're messing with the command handler code."
+        )
+
+    is_logged_in = await evt.sender.is_logged_in()
+
+    if "mxid" in status:
+        ok, coro = await cleanup_old_portal_while_bridging(evt, portal)
+        if not ok:
+            return None
+        elif coro:
+            asyncio.create_task(coro)
+            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
+    async with portal._create_room_lock:
+        await _locked_confirm_bridge(
+            evt, portal=portal, room_id=bridge_to_mxid, is_logged_in=is_logged_in
+        )
+
+async def _locked_confirm_bridge(
+    evt: CommandEvent, portal: po.Portal, room_id: RoomID, is_logged_in: bool
+) -> EventID | None:
+    try:
+        group = await evt.bridge.signal.get_group(
+            evt.sender.username, portal.chat_id, portal.revision
+        )
+    except Exception:
+        evt.log.exception("Failed to get_group(%s) for manual bridging.", portal.chat_id)
+        if is_logged_in:
+            return await evt.reply(
+                "Failed to get info of signal chat. You are logged in, are you in that chat?"
+            )
+        else:
+            return await evt.reply(
+                "Failed to get info of signal chat. "
+                "You're not logged in, this should not happen."
+            )
+
+    portal.mxid = room_id
+    portal.by_mxid[portal.mxid] = portal
+    (portal.title, portal.about, levels, portal.encrypted, portal.photo_id) = await get_initial_state(
+        evt.az.intent, evt.room_id
+    )
+    await portal.save()
+    await portal.update_bridge_info()
+
+    asyncio.create_task(portal.update_matrix_room(evt.sender, group))
+
+    await warn_missing_power(levels, evt)
+
+    return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
 
 
 async def get_initial_state(
 async def get_initial_state(
     intent: IntentAPI, room_id: RoomID
     intent: IntentAPI, room_id: RoomID
@@ -454,3 +556,16 @@ async def get_initial_state(
             # Some state event probably has empty content
             # Some state event probably has empty content
             pass
             pass
     return title, about, levels, encrypted, avatar_url
     return title, about, levels, encrypted, avatar_url
+
+async def warn_missing_power(levels: PowerLevelStateEventContent, evt: CommandEvent) -> None:
+    bot_pl = levels.get_user_level(evt.az.bot_mxid)
+    if bot_pl < levels.get_event_level(EventType.ROOM_POWER_LEVELS):
+        await evt.reply(missing_power_warning.format(bot_mxid=evt.az.bot_mxid))
+    elif bot_pl <= 50:
+        await evt.reply(low_power_warning.format(bot_mxid=evt.az.bot_mxid))
+    if levels.state_default < 50 and (
+        levels.events[EventType.ROOM_NAME] >= 50
+        or levels.events[EventType.ROOM_AVATAR] >= 50
+        or levels.events[EventType.ROOM_TOPIC] >= 50
+    ):
+        await evt.reply(meta_power_warning)

+ 1 - 0
mautrix_signal/util/__init__.py

@@ -1,3 +1,4 @@
 from .color_log import ColorFormatter
 from .color_log import ColorFormatter
 from .id_to_str import id_to_str
 from .id_to_str import id_to_str
 from .normalize_number import normalize_number
 from .normalize_number import normalize_number
+from .user_has_power_level import user_has_power_level

+ 35 - 0
mautrix_signal/util/user_has_power_level.py

@@ -0,0 +1,35 @@
+# mautrix-signal - A Matrix-Signal puppeting bridge
+# Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
+from __future__ import annotations
+
+from mautrix.appservice import IntentAPI
+from mautrix.errors import MatrixRequestError
+from mautrix.types import EventType, RoomID
+
+from .. import user as u
+
+async def user_has_power_level(
+    room_id: RoomID, intent: IntentAPI, sender: u.User, event: str
+) -> bool:
+    if sender.is_admin:
+        return True
+    # Make sure the state store contains the power levels.
+    try:
+        await intent.get_power_levels(room_id)
+    except MatrixRequestError:
+        return False
+    event_type = EventType.find(f"net.maunium.signal.{event}", t_class=EventType.Class.STATE)
+    return await intent.state_store.has_power_level(room_id, sender.mxid, event_type)