Skip to content

Commit

Permalink
Complete bridge command, fix minor id command issue
Browse files Browse the repository at this point in the history
  • Loading branch information
MaximilianGaedig committed Jul 30, 2022
1 parent 10d0b96 commit b2321fb
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 48 deletions.
211 changes: 163 additions & 48 deletions mautrix_signal/commands/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
import base64
import json

from typing import Awaitable
import asyncio

from mausignald.errors import UnknownIdentityKey, UnregisteredUserError
from mausignald.types import Address, GroupID, TrustLevel
from mautrix.appservice import IntentAPI
from mautrix.bridge.commands import SECTION_ADMIN, HelpSection, command_handler
from mautrix.types import ContentURI, EventID, EventType, PowerLevelStateEventContent, RoomID

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 .typehint import CommandEvent

Expand Down Expand Up @@ -100,6 +103,8 @@ async def pm(evt: CommandEvent) -> None:
)
await portal.main_intent.invite_user(portal.mxid, evt.sender.mxid)
return


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")

Expand Down Expand Up @@ -337,17 +342,7 @@ async def create(evt: CommandEvent) -> EventID:
receiver="",
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 evt.reply(f"Signal chat created. ID: {portal.chat_id}")
Expand All @@ -361,7 +356,7 @@ async def create(evt: CommandEvent) -> EventID:
)
async def get_id(evt: CommandEvent) -> EventID:
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.")


Expand All @@ -370,62 +365,169 @@ async def get_id(evt: CommandEvent) -> EventID:
management_only=False,
help_section=SECTION_SIGNAL,
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:
if len(evt.args) == 0:
return await evt.reply(
"**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
try:
chat_id= GroupID(evt.args[0])
chat_id = GroupID(evt.args[0])
except ValueError:
pass
if not chat_id:
return await evt.reply(
"That doesn't seem like a Signal chat ID.\n\n"
"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:
await evt.reply(
has_portal_message = (
"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(
intent: IntentAPI, room_id: RoomID
Expand Down Expand Up @@ -454,3 +556,16 @@ async def get_initial_state(
# Some state event probably has empty content
pass
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 change: 1 addition & 0 deletions mautrix_signal/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .color_log import ColorFormatter
from .id_to_str import id_to_str
from .normalize_number import normalize_number
from .user_has_power_level import user_has_power_level
35 changes: 35 additions & 0 deletions mautrix_signal/util/user_has_power_level.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit b2321fb

Please sign in to comment.