Jelajahi Sumber

Add support for kick/ban/unban from Matrix (#257)

Malte E 3 tahun lalu
induk
melakukan
0c7b46e45c
5 mengubah file dengan 135 tambahan dan 2 penghapusan
  1. 2 2
      ROADMAP.md
  2. 22 0
      mausignald/signald.py
  3. 7 0
      mausignald/types.py
  4. 51 0
      mautrix_signal/matrix.py
  5. 53 0
      mautrix_signal/portal.py

+ 2 - 2
ROADMAP.md

@@ -17,11 +17,11 @@
   * [x] Group info changes
     * [x] Name
     * [x] Avatar
-  * [ ] Membership actions
+  * [x] Membership actions
     * [x] Join (accept invite)
     * [x] Invite
     * [x] Leave
-    * [ ] Kick
+    * [x] Kick/Ban/Unban
   * [ ] Typing notifications
   * [ ] Read receipts (currently partial support, only marks last message)
   * [x] Delivery receipts (sent after message is bridged)

+ 22 - 0
mausignald/signald.py

@@ -345,6 +345,28 @@ class SignaldClient(SignaldRPCClient):
     async def leave_group(self, username: str, group_id: GroupID) -> None:
         await self.request_v1("leave_group", account=username, groupID=group_id)
 
+    async def ban_user(
+        self, username: str, group_id: GroupID, users: list[Address]
+    ) -> Group | GroupV2:
+        serialized_users = [user.serialize() for user in (users or [])]
+        resp = await self.request_v1(
+            "ban_user", account=username, group_id=group_id, users=serialized_users
+        )
+        legacy = [Group.deserialize(group) for group in resp.get("legacyGroups", [])]
+        v2 = [GroupV2.deserialize(group) for group in resp.get("groups", [])]
+        return legacy + v2
+
+    async def unban_user(
+        self, username: str, group_id: GroupID, users: list[Address]
+    ) -> Group | GroupV2:
+        serialized_users = [user.serialize() for user in (users or [])]
+        resp = await self.request_v1(
+            "unban_user", account=username, group_id=group_id, users=serialized_users
+        )
+        legacy = [Group.deserialize(group) for group in resp.get("legacyGroups", [])]
+        v2 = [GroupV2.deserialize(group) for group in resp.get("groups", [])]
+        return legacy + v2
+
     async def update_group(
         self,
         username: str,

+ 7 - 0
mausignald/types.py

@@ -217,6 +217,12 @@ class GroupMember(SerializableAttrs):
     role: GroupMemberRole = GroupMemberRole.UNKNOWN
 
 
+@dataclass
+class BannedGroupMember(SerializableAttrs):
+    uuid: UUID
+    timestamp: int
+
+
 @dataclass(kw_only=True)
 class GroupV2(GroupV2ID, SerializableAttrs):
     title: str
@@ -236,6 +242,7 @@ class GroupV2(GroupV2ID, SerializableAttrs):
     )
     requesting_members: List[Address] = field(factory=lambda: [], json="requestingMembers")
     announcements: AnnouncementsMode = field(default=AnnouncementsMode.UNKNOWN)
+    banned_members: Optional[List[BannedGroupMember]] = None
 
 
 @dataclass

+ 51 - 0
mautrix_signal/matrix.py

@@ -100,6 +100,57 @@ class MatrixHandler(BaseMatrixHandler):
 
         await portal.handle_matrix_join(user)
 
+    async def handle_kick_ban(
+        self,
+        action: str,
+        room_id: RoomID,
+        user_id: UserID,
+        sender: UserID,
+        reason: str,
+        event_id: EventID,
+    ) -> None:
+        self.log.debug(f"{user_id} was {action} from {room_id} by {sender} for {reason}")
+        portal = await po.Portal.get_by_mxid(room_id)
+        if not portal:
+            return
+
+        if user_id == self.az.bot_mxid:
+            if portal.is_direct:
+                await portal.unbridge()
+            return
+
+        sender = await u.User.get_by_mxid(sender)
+        sender, is_relay = await portal.get_relay_sender(sender, "kick/ban")
+        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
+        if action == "banned":
+            await portal.ban_matrix(user, sender)
+        elif action == "kicked":
+            await portal.kick_matrix(user, sender)
+        else:
+            await portal.unban_matrix(user, sender)
+
+    async def handle_kick(
+        self, room_id: RoomID, user_id: UserID, kicked_by: UserID, reason: str, event_id: EventID
+    ) -> None:
+        await self.handle_kick_ban("kicked", room_id, user_id, kicked_by, reason, event_id)
+
+    async def handle_unban(
+        self, room_id: RoomID, user_id: UserID, unbanned_by: UserID, reason: str, event_id: EventID
+    ) -> None:
+        await self.handle_kick_ban("unbanned", room_id, user_id, unbanned_by, reason, event_id)
+
+    async def handle_ban(
+        self, room_id: RoomID, user_id: UserID, banned_by: UserID, reason: str, event_id: EventID
+    ) -> None:
+        await self.handle_kick_ban("banned", room_id, user_id, banned_by, reason, event_id)
+
     @classmethod
     async def handle_reaction(
         cls, room_id: RoomID, user_id: UserID, event_id: EventID, content: ReactionEventContent

+ 53 - 0
mautrix_signal/portal.py

@@ -770,6 +770,59 @@ class Portal(DBPortal, BasePortal):
                 await self.signal.leave_group(user.username, self.chat_id)
             # TODO cleanup if empty
 
+    async def kick_matrix(self, user: u.User | p.Puppet, source: u.User) -> None:
+        try:
+            await self.signal.update_group(
+                source.username, self.chat_id, remove_members=[user.address]
+            )
+        except Exception as e:
+            self.log.exception(f"Failed to kick Signal user: {e}")
+            info = await self.signal.get_group(source.username, self.chat_id)
+            if user.address in info.members:
+                await self.main_intent.invite_user(
+                    self.mxid,
+                    user.mxid,
+                    check_cache=True,
+                    reason=f"Failed to kick Signal user: {e}",
+                )
+                await user.intent_for(self).ensure_joined(self.mxid)
+
+    async def ban_matrix(self, user: u.User | p.Puppet, source: u.User) -> None:
+        try:
+            await self.signal.ban_user(source.username, self.chat_id, users=[user.address])
+        except Exception as e:
+            self.log.exception(f"Failed to ban Signal user: {e}")
+            info = await self.signal.get_group(source.username, self.chat_id)
+            is_banned = False
+            if info.banned_members:
+                for member in info.banned_members:
+                    is_banned = user.address.uuid == member.uuid or is_banned
+            if not is_banned:
+                await self.main_intent.unban_user(
+                    self.mxid, user.mxid, reason=f"Failed to ban Signal user: {e}"
+                )
+            if user.address in info.members:
+                await self.main_intent.invite_user(
+                    self.mxid,
+                    user.mxid,
+                    check_cache=True,
+                )
+                await user.intent_for(self).ensure_joined(self.mxid)
+
+    async def unban_matrix(self, user: u.User | p.Puppet, source: u.User) -> None:
+        try:
+            await self.signal.unban_user(source.username, self.chat_id, users=[user.address])
+        except Exception as e:
+            self.log.exception(f"Failed to unban Signal user: {e}")
+            info = await self.signal.get_group(source.username, self.chat_id)
+            if info.banned_members:
+                for member in info.banned_members:
+                    if member.uuid == user.address.uuid:
+                        await self.main_intent.ban_user(
+                            self.mxid, user.mxid, reason=f"Failed to unban Signal user: {e}"
+                        )
+                        return
+
     async def handle_matrix_invite(self, invited_by: u.User, user: u.User | p.Puppet) -> None:
         if self.is_direct:
             raise RejectMatrixInvite("You can't invite additional users to private chats.")