Ver Fonte

implement handle_puppet_group_invite

Malte E há 2 anos atrás
pai
commit
87ca67dd9f

+ 1 - 1
ROADMAP.md

@@ -62,7 +62,7 @@
   * [ ] Provisioning API for logging in
     * [x] Linking as secondary device
     * [ ] Registering as primary device
-  * [x] Private chat creation by inviting Matrix puppet of Signal user to new room
+  * [x] Private chat/group creation by inviting Matrix puppet of Signal user to new room
   * [x] Option to use own Matrix account for messages sent from other Signal clients
     * [x] Automatic login with shared secret
     * [x] Manual login with `login-matrix`

+ 1 - 41
mautrix_signal/commands/signal.py

@@ -22,7 +22,6 @@ import json
 
 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,
@@ -37,6 +36,7 @@ from .. import portal as po, puppet as pu
 from ..util import normalize_number, user_has_power_level
 from .auth import make_qr
 from .typehint import CommandEvent
+from .util import get_initial_state
 
 try:
     import PIL as _
@@ -354,7 +354,6 @@ async def create(evt: CommandEvent) -> EventID:
     await warn_missing_power(levels, evt)
 
     await portal.create_signal_group(evt.sender, levels, join_rule)
-    await evt.reply(f"Signal chat created. ID: {portal.chat_id}")
 
 
 @command_handler(
@@ -535,45 +534,6 @@ async def _locked_confirm_bridge(
     return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
 
 
-async def get_initial_state(
-    intent: IntentAPI, room_id: RoomID
-) -> tuple[
-    str | None,
-    str | None,
-    PowerLevelStateEventContent | None,
-    bool,
-    ContentURI | None,
-    JoinRule | None,
-]:
-    state = await intent.get_state(room_id)
-    title: str | None = None
-    about: str | None = None
-    levels: PowerLevelStateEventContent | None = None
-    encrypted: bool = False
-    avatar_url: ContentURI | None = None
-    join_rule: JoinRule | None = None
-    for event in state:
-        try:
-            if event.type == EventType.ROOM_NAME:
-                title = event.content.name
-            elif event.type == EventType.ROOM_TOPIC:
-                about = event.content.topic
-            elif event.type == EventType.ROOM_POWER_LEVELS:
-                levels = event.content
-            elif event.type == EventType.ROOM_CANONICAL_ALIAS:
-                title = title or event.content.canonical_alias
-            elif event.type == EventType.ROOM_ENCRYPTION:
-                encrypted = True
-            elif event.type == EventType.ROOM_AVATAR:
-                avatar_url = event.content.url
-            elif event.type == EventType.ROOM_JOIN_RULES:
-                join_rule = event.content.join_rule
-        except KeyError:
-            # Some state event probably has empty content
-            pass
-    return title, about, levels, encrypted, avatar_url, join_rule
-
-
 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):

+ 58 - 0
mautrix_signal/commands/util.py

@@ -0,0 +1,58 @@
+# mautrix-signal - A Matrix-Signal puppeting bridge
+# Copyright (C) 2021 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.types import ContentURI, EventType, JoinRule, PowerLevelStateEventContent, RoomID
+
+
+async def get_initial_state(
+    intent: IntentAPI, room_id: RoomID
+) -> tuple[
+    str | None,
+    str | None,
+    PowerLevelStateEventContent | None,
+    bool,
+    ContentURI | None,
+    JoinRule | None,
+]:
+    state = await intent.get_state(room_id)
+    title: str | None = None
+    about: str | None = None
+    levels: PowerLevelStateEventContent | None = None
+    encrypted: bool = False
+    avatar_url: ContentURI | None = None
+    join_rule: JoinRule | None = None
+    for event in state:
+        try:
+            if event.type == EventType.ROOM_NAME:
+                title = event.content.name
+            elif event.type == EventType.ROOM_TOPIC:
+                about = event.content.topic
+            elif event.type == EventType.ROOM_POWER_LEVELS:
+                levels = event.content
+            elif event.type == EventType.ROOM_CANONICAL_ALIAS:
+                title = title or event.content.canonical_alias
+            elif event.type == EventType.ROOM_ENCRYPTION:
+                encrypted = True
+            elif event.type == EventType.ROOM_AVATAR:
+                avatar_url = event.content.url
+            elif event.type == EventType.ROOM_JOIN_RULES:
+                join_rule = event.content.join_rule
+        except KeyError:
+            # Some state event probably has empty content
+            pass
+    return title, about, levels, encrypted, avatar_url, join_rule

+ 1 - 0
mautrix_signal/config.py

@@ -64,6 +64,7 @@ class Config(BaseBridgeConfig):
         copy("bridge.sync_direct_chat_list")
         copy("bridge.double_puppet_server_map")
         copy("bridge.double_puppet_allow_discovery")
+        copy("bridge.create_group_on_invite")
         if self["bridge.login_shared_secret"]:
             base["bridge.login_shared_secret_map"] = {
                 base["homeserver.domain"]: self["bridge.login_shared_secret"]

+ 3 - 0
mautrix_signal/example-config.yaml

@@ -234,6 +234,9 @@ bridge:
     periodic_sync: 0
     # Should leaving the room on Matrix make the user leave on Signal?
     bridge_matrix_leave: true
+    # Should the bridge auto-create a group chat on Signal when a ghost is invited to a room?
+    # Requires the user to have sufficient power level and double puppeting enabled.
+    create_group_on_invite: true
 
     # Provisioning API part of the web server for automated portal creation and fetching information.
     # Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager).

+ 52 - 1
mautrix_signal/matrix.py

@@ -17,7 +17,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
-from mausignald.types import Address
+from mausignald.types import Address, GroupID
 from mautrix.bridge import BaseMatrixHandler, RejectMatrixInvite
 from mautrix.types import (
     Event,
@@ -37,6 +37,7 @@ from mautrix.types import (
 )
 
 from . import portal as po, puppet as pu, signal as s, user as u
+from .commands.util import get_initial_state
 from .db import Message as DBMessage
 
 if TYPE_CHECKING:
@@ -55,6 +56,56 @@ class MatrixHandler(BaseMatrixHandler):
 
         super().__init__(bridge=bridge)
 
+    async def handle_puppet_group_invite(
+        self,
+        room_id: RoomID,
+        puppet: pu.Puppet,
+        invited_by: u.User,
+        evt: StateEvent,
+        members: list[UserID],
+    ) -> None:
+        double_puppet = await pu.Puppet.get_by_custom_mxid(invited_by.mxid)
+        if (
+            not double_puppet
+            or self.az.bot_mxid in members
+            or not self.config["bridge.create_group_on_invite"]
+        ):
+            if self.az.bot_mxid not in members:
+                await puppet.default_mxid_intent.leave_room(
+                    room_id,
+                    reason="This ghost does not join multi-user rooms without the bridge bot.",
+                )
+            else:
+                await puppet.default_mxid_intent.send_notice(
+                    room_id,
+                    "This ghost will remain inactive "
+                    "until a Signal Group is created for this room.",
+                )
+            return
+
+        await double_puppet.intent.invite_user(room_id, self.az.bot_mxid)
+
+        title, about, levels, encrypted, avatar_url, join_rule = await get_initial_state(
+            double_puppet.intent, room_id
+        )
+
+        portal = po.Portal(
+            chat_id=GroupID(""),
+            mxid=evt.room_id,
+            name=title,
+            topic=about or "",
+            encrypted=encrypted,
+            receiver="",
+            avatar_url=avatar_url,
+        )
+        await portal.az.intent.ensure_joined(room_id)
+        invited_by_level = levels.get_user_level(invited_by.mxid)
+        if invited_by_level > levels.get_user_level(self.az.bot_mxid):
+            levels.users[self.az.bot_mxid] = 100 if invited_by_level >= 100 else invited_by_level
+            await double_puppet.intent.set_power_levels(room_id, levels)
+
+        await portal.create_signal_group(invited_by, levels, join_rule)
+
     async def handle_invite(
         self, room_id: RoomID, user_id: UserID, inviter: u.User, event_id: EventID
     ) -> None:

+ 1 - 0
mautrix_signal/portal.py

@@ -1805,6 +1805,7 @@ class Portal(DBPortal, BasePortal):
         await self.update_bridge_info()
         if relaybot:
             await self._handle_relaybot_invited(relaybot)
+        await self.main_intent.send_notice(self.mxid, f"Signal group created. ID: {self.chat_id}")
 
     async def bridge_signal_group(
         self, source: u.User, levels: PowerLevelStateEventContent