瀏覽代碼

bridge join via invite link to knock, bridge ban/unban (S->M)

Malte E 2 年之前
父節點
當前提交
882f9c13a1
共有 4 個文件被更改,包括 163 次插入4 次删除
  1. 4 3
      ROADMAP.md
  2. 22 0
      mausignald/signald.py
  3. 38 0
      mautrix_signal/matrix.py
  4. 99 1
      mautrix_signal/portal.py

+ 4 - 3
ROADMAP.md

@@ -45,11 +45,12 @@
     * [ ] Real time
       * [x] Groups
       * [ ] Users
-  * [ ] Membership actions
+  * [x] Membership actions
     * [x] Join
     * [x] Invite
-    * [ ] Request join (via invite link)
-    * [x] Kick / leave
+    * [x] Request join (via invite link, requires a client that supports knocks)
+    * [x] Leave
+    * [x] Kick/Ban/Unban
   * [x] Group permissions
   * [x] Typing notifications
   * [x] Read receipts

+ 22 - 0
mausignald/signald.py

@@ -369,6 +369,28 @@ class SignaldClient(SignaldRPCClient):
         )
         return GroupV2.deserialize(resp)
 
+    async def approve_membership(
+        self, username: str, group_id: GroupID, members: list[Address]
+    ) -> GroupV2:
+        serialized_members = [member.serialize() for member in (members or [])]
+        resp = await self.request_v1(
+            "approve_membership", account=username, groupID=group_id, members=serialized_members
+        )
+        return GroupV2.deserialize(resp)
+
+    async def refuse_membership(
+        self, username: str, group_id: GroupID, members: list[Address], also_ban: bool = False
+    ) -> GroupV2:
+        serialized_members = [member.serialize() for member in (members or [])]
+        resp = await self.request_v1(
+            "refuse_membership",
+            account=username,
+            group_id=group_id,
+            members=serialized_members,
+            also_ban=also_ban,
+        )
+        return GroupV2.deserialize(resp)
+
     async def update_group(
         self,
         username: str,

+ 38 - 0
mautrix_signal/matrix.py

@@ -152,6 +152,44 @@ class MatrixHandler(BaseMatrixHandler):
     ) -> None:
         await self.handle_kick_ban("banned", room_id, user_id, banned_by, reason, event_id)
 
+    async def handle_accept_knock(
+        self, room_id: RoomID, user_id: UserID, sender: UserID, reason: str, event_id: EventID
+    ) -> None:
+        self.log.debug(f"the knock of {user_id} on room {room_id} was accepted: {reason}")
+        portal = await po.Portal.get_by_mxid(room_id)
+        if not portal:
+            return
+        sender = await u.User.get_by_mxid(sender)
+        sender, is_relay = await portal.get_relay_sender(sender, "accept knock")
+        if not sender:
+            return
+
+        user = await pu.Puppet.get_by_mxid(user_id)
+        if not user:
+            user = await u.User.get_by_mxid(user_id, create=False)
+            if not user or not await user.is_logged_in():
+                return
+        await portal.matrix_accept_knock(sender, user)
+
+    async def handle_reject_knock(
+        self, room_id: RoomID, user_id: UserID, sender: UserID, reason: str, event_id: EventID
+    ) -> None:
+        self.log.debug(f"the knock of {user_id} on room {room_id} was rejected: {reason}")
+        portal = await po.Portal.get_by_mxid(room_id)
+        if not portal:
+            return
+        sender = await u.User.get_by_mxid(sender)
+        sender, is_relay = await portal.get_relay_sender(sender, "accept knock")
+        if not sender:
+            return
+
+        user = await pu.Puppet.get_by_mxid(user_id)
+        if not user:
+            user = await u.User.get_by_mxid(user_id, create=False)
+            if not user or not await user.is_logged_in():
+                return
+        await portal.matrix_reject_knock(sender, user)
+
     @classmethod
     async def handle_reaction(
         cls, room_id: RoomID, user_id: UserID, event_id: EventID, content: ReactionEventContent

+ 99 - 1
mautrix_signal/portal.py

@@ -1067,6 +1067,42 @@ class Portal(DBPortal, BasePortal):
             self.log.exception(f"Failed to update Signal link access control: {e}")
             await self._update_join_rules(
                 await self.signal.get_group(sender.username, self.chat_id)
+                )
+
+    async def matrix_accept_knock(self, sender: u.User, user: p.Puppet | u.User) -> None:
+        try:
+            await self.signal.approve_membership(
+                sender.username, self.chat_id, members=[user.address]
+            )
+            if isinstance(user, p.Puppet):
+                await user.intent_for(self).ensure_joined(self.mxid)
+        except RPCError as e:
+            raise RejectMatrixInvite(str(e)) from e
+        power_levels = await self.main_intent.get_power_levels(self.mxid)
+        invitee_pl = power_levels.get_user_level(user.mxid)
+        if invitee_pl >= 50:
+            group_member = GroupMember(uuid=user.uuid, role=GroupMemberRole.ADMINISTRATOR)
+            try:
+                update_meta = await self.signal.update_group(
+                    sender.username, self.chat_id, update_role=group_member
+                )
+                self.revision = update_meta.revision
+            except Exception as e:
+                self.log.exception(f"Failed to update Signal member role: {e}")
+                await self._update_power_levels(
+                    await self.signal.get_group(sender.username, self.chat_id)
+                )
+
+    async def matrix_reject_knock(self, sender: u.User, user: p.Puppet | u.User) -> None:
+        try:
+            await self.signal.refuse_membership(
+                sender.username, self.chat_id, members=[user.address]
+            )
+        except RPCError as e:
+            await user.intent_for(self).knock(
+                self.mxid,
+                reason=f"refusing membership failed: {e}",
+                servers=[self.config["homeserver.domain"]],
             )
 
     # endregion
@@ -2020,12 +2056,61 @@ class Portal(DBPortal, BasePortal):
         remove_users: set[UserID] = {
             UserID(evt.state_key)
             for evt in member_events
-            if evt.content.membership == Membership.JOIN and evt.state_key != self.az.bot_mxid
+            if (
+                evt.content.membership == Membership.JOIN
+                or evt.content.membership == Membership.INVITE
+                or evt.content.membership == Membership.KNOCK
+            )
+            and evt.state_key != self.az.bot_mxid
+        }
+        unban_users: set[UserID] = {
+            UserID(evt.state_key)
+            for evt in member_events
+            if evt.content.membership == Membership.BAN and evt.state_key != self.az.bot_mxid
         }
 
         pending_members = info.pending_members if isinstance(info, GroupV2) else []
+        requesting_members = info.requesting_members if isinstance(info, GroupV2) else []
+        banned_members = info.banned_members if isinstance(info, GroupV2) else []
         self._pending_members = {addr.uuid for addr in pending_members}
 
+        for member in banned_members:
+            user = await u.User.get_by_uuid(member.uuid)
+            if user:
+                unban_users.discard(user.mxid)
+                remove_users.discard(user.mxid)
+                try:
+                    await self.main_intent.ban_user(
+                        self.mxid, user.mxid, reason="Banned on Signal"
+                    )
+                except (MForbidden, MBadState) as e:
+                    self.log.debug(f"could not ban {user.mxid}: {e}")
+            puppet = await p.Puppet.get_by_address(Address(uuid=member.uuid))
+            unban_users.discard(puppet.mxid)
+            remove_users.discard(puppet.mxid)
+            try:
+                await self.main_intent.ban_user(self.mxid, puppet.mxid, reason="Banned on Signal")
+            except (MForbidden, MBadState) as e:
+                self.log.debug(f"could not ban {puppet.mxid}: {e}")
+
+        for mxid in unban_users:
+            user = await u.User.get_by_mxid(mxid, create=False)
+            if user and await user.is_logged_in():
+                try:
+                    await self.main_intent.unban_user(
+                        self.mxid, user.mxid, reason="Unbanned on Signal"
+                    )
+                except (MForbidden, MBadState) as e:
+                    self.log.debug(f"could not unban {user.mxid}: {e}")
+            puppet = await p.Puppet.get_by_mxid(mxid, create=False)
+            if puppet:
+                try:
+                    await self.main_intent.unban_user(
+                        self.mxid, puppet.mxid, reason="Unbanned on Signal"
+                    )
+                except (MForbidden, MBadState) as e:
+                    self.log.debug(f"could not unban {puppet.mxid}: {e}")
+
         for address in info.members + pending_members:
             user = await u.User.get_by_address(address)
             if user:
@@ -2053,6 +2138,19 @@ class Portal(DBPortal, BasePortal):
                 await puppet.intent_for(self).ensure_joined(self.mxid)
             remove_users.discard(puppet.default_mxid)
 
+        for address in requesting_members:
+            puppet = await p.Puppet.get_by_address(address)
+            if puppet:
+                remove_users.discard(puppet.mxid)
+                try:
+                    await puppet.intent_for(self).knock_room(
+                        self.mxid,
+                        reason="via invite link",
+                        servers=[self.config["homeserver.domain"]],
+                    )
+                except (MForbidden, IntentError) as e:
+                    self.log.debug(f"failed to bridge knock: {e}")
+
         for mxid in remove_users:
             user = await u.User.get_by_mxid(mxid, create=False)
             if user and await user.is_logged_in():