Răsfoiți Sursa

Sync power levels from Signal

Tulir Asokan 4 ani în urmă
părinte
comite
6c2d56790a
3 a modificat fișierele cu 100 adăugiri și 13 ștergeri
  1. 1 1
      ROADMAP.md
  2. 40 4
      mausignald/types.py
  3. 59 8
      mautrix_signal/portal.py

+ 1 - 1
ROADMAP.md

@@ -40,7 +40,7 @@
     * [ ] Real time
     * [ ] Real time
       * [x] Groups
       * [x] Groups
       * [ ] Users
       * [ ] Users
-  * [ ] Group permissions
+  * [x] Group permissions
   * [x] Typing notifications
   * [x] Typing notifications
   * [x] Read receipts
   * [x] Read receipts
   * [ ] Delivery receipts (there's no good way to bridge these)
   * [ ] Delivery receipts (there's no good way to bridge these)

+ 40 - 4
mausignald/types.py

@@ -126,18 +126,54 @@ class GroupV2ID(SerializableAttrs['GroupV2ID']):
     revision: Optional[int] = None
     revision: Optional[int] = None
 
 
 
 
+class AccessControlMode(SerializableEnum):
+    UNKNOWN = "UNKNOWN"
+    ANY = "ANY"
+    MEMBER = "MEMBER"
+    ADMINISTRATOR = "ADMINISTRATOR"
+    UNSATISFIABLE = "UNSATISFIABLE"
+    UNRECOGNIZED = "UNRECOGNIZED"
+
+
 @dataclass
 @dataclass
+class GroupAccessControl(SerializableAttrs['GroupAccessControl']):
+    attributes: AccessControlMode = AccessControlMode.UNKNOWN
+    link: AccessControlMode = AccessControlMode.UNKNOWN
+    members: AccessControlMode = AccessControlMode.UNKNOWN
+
+
+class GroupMemberRole(SerializableEnum):
+    UNKNOWN = "UNKNOWN"
+    DEFAULT = "DEFAULT"
+    ADMINISTRATOR = "ADMINISTRATOR"
+    UNRECOGNIZED = "UNRECOGNIZED"
+
+
+@dataclass
+class GroupMember(SerializableAttrs['GroupMember']):
+    uuid: UUID
+    joined_revision: int = 0
+    role: GroupMemberRole = GroupMemberRole.UNKNOWN
+
+
+@dataclass(kw_only=True)
 class GroupV2(GroupV2ID, SerializableAttrs['GroupV2']):
 class GroupV2(GroupV2ID, SerializableAttrs['GroupV2']):
     title: str
     title: str
-    members: List[Address]
     avatar: Optional[str] = None
     avatar: Optional[str] = None
+    timer: Optional[int] = None
+    master_key: Optional[str] = attr.ib(default=None, metadata={"json": "masterKey"})
+    invite_link: Optional[str] = attr.ib(default=None, metadata={"json": "inviteLink"})
+    access_control: GroupAccessControl = attr.ib(factory=lambda: GroupAccessControl(),
+                                                 metadata={"json": "accessControl"})
+    members: List[Address]
+    member_detail: List[GroupMember] = attr.ib(factory=lambda: [],
+                                               metadata={"json": "memberDetail"})
     pending_members: List[Address] = attr.ib(factory=lambda: [],
     pending_members: List[Address] = attr.ib(factory=lambda: [],
                                              metadata={"json": "pendingMembers"})
                                              metadata={"json": "pendingMembers"})
+    pending_member_detail: List[GroupMember] = attr.ib(factory=lambda: [],
+                                                       metadata={"json": "pendingMemberDetail"})
     requesting_members: List[Address] = attr.ib(factory=lambda: [],
     requesting_members: List[Address] = attr.ib(factory=lambda: [],
                                                 metadata={"json": "requestingMembers"})
                                                 metadata={"json": "requestingMembers"})
-    master_key: Optional[str] = attr.ib(default=None, metadata={"json": "masterKey"})
-    invite_link: Optional[str] = attr.ib(default=None, metadata={"json": "inviteLink"})
-    timer: Optional[int] = None
 
 
 
 
 @dataclass
 @dataclass

+ 59 - 8
mautrix_signal/portal.py

@@ -25,12 +25,13 @@ import time
 import os
 import os
 
 
 from mausignald.types import (Address, MessageData, Reaction, Quote, Group, Contact, Profile,
 from mausignald.types import (Address, MessageData, Reaction, Quote, Group, Contact, Profile,
-                              Attachment, GroupID, GroupV2ID, GroupV2, Mention, Sticker)
+                              Attachment, GroupID, GroupV2ID, GroupV2, Mention, Sticker,
+                              GroupAccessControl, AccessControlMode, GroupMemberRole)
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.bridge import BasePortal, async_getter_lock
 from mautrix.bridge import BasePortal, async_getter_lock
 from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType,
 from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType,
                            MessageEvent, EncryptedEvent, ContentURI, MediaMessageEventContent,
                            MessageEvent, EncryptedEvent, ContentURI, MediaMessageEventContent,
-                           ImageInfo, VideoInfo, FileInfo, AudioInfo)
+                           ImageInfo, VideoInfo, FileInfo, AudioInfo, PowerLevelStateEventContent)
 from mautrix.errors import MatrixError, MForbidden
 from mautrix.errors import MatrixError, MForbidden
 
 
 from .db import Portal as DBPortal, Message as DBMessage, Reaction as DBReaction
 from .db import Portal as DBPortal, Message as DBMessage, Reaction as DBReaction
@@ -632,6 +633,10 @@ class Portal(DBPortal, BasePortal):
             raise ValueError(f"Unexpected type for group update_info: {type(info)}")
             raise ValueError(f"Unexpected type for group update_info: {type(info)}")
         changed = await self._update_avatar(info, sender) or changed
         changed = await self._update_avatar(info, sender) or changed
         await self._update_participants(source, info.members)
         await self._update_participants(source, info.members)
+        try:
+            await self._update_power_levels(info)
+        except Exception:
+            self.log.warning("Error updating power levels", exc_info=True)
         if changed:
         if changed:
             await self.update_bridge_info()
             await self.update_bridge_info()
             await self.update()
             await self.update()
@@ -720,6 +725,14 @@ class Portal(DBPortal, BasePortal):
                 await source.sync_contact(address)
                 await source.sync_contact(address)
             await puppet.intent_for(self).ensure_joined(self.mxid)
             await puppet.intent_for(self).ensure_joined(self.mxid)
 
 
+    async def _update_power_levels(self, info: ChatInfo) -> None:
+        if not self.mxid:
+            return
+
+        power_levels = await self.main_intent.get_power_levels(self.mxid)
+        power_levels = await self._get_power_levels(power_levels, info=info, is_initial=False)
+        await self.main_intent.set_power_levels(self.mxid, power_levels)
+
     # endregion
     # endregion
     # region Bridge info state event
     # region Bridge info state event
 
 
@@ -802,6 +815,46 @@ class Portal(DBPortal, BasePortal):
 
 
         await self.update_info(source, info)
         await self.update_info(source, info)
 
 
+    async def _get_power_levels(self, levels: Optional[PowerLevelStateEventContent] = None,
+                                info: Optional[ChatInfo] = None, is_initial: bool = False
+                                ) -> PowerLevelStateEventContent:
+        levels = levels or PowerLevelStateEventContent()
+        levels.events[EventType.ROOM_ENCRYPTION] = 50 if self.matrix.e2ee else 99
+        levels.events[EventType.ROOM_TOMBSTONE] = 99
+        # Remote delete is only for your own messages
+        levels.redact = 99
+        if self.is_direct:
+            levels.ban = 99
+            levels.kick = 99
+            levels.invite = 99
+            levels.events[EventType.ROOM_NAME] = 0
+            levels.events[EventType.ROOM_AVATAR] = 0
+            levels.events[EventType.ROOM_TOPIC] = 0
+            levels.state_default = 0
+            levels.users_default = 0
+            levels.events_default = 0
+        else:
+            if isinstance(info, GroupV2):
+                ac = info.access_control
+                for detail in info.member_detail:
+                    puppet = await p.Puppet.get_by_address(Address(uuid=detail.uuid))
+                    level = 50 if detail.role == GroupMemberRole.ADMINISTRATOR else 0
+                    print(puppet.mxid, detail, level)
+                    levels.users[puppet.intent_for(self).mxid] = level
+            else:
+                ac = GroupAccessControl()
+            levels.ban = 50
+            levels.kick = 50
+            levels.invite = 50 if ac.members == AccessControlMode.ADMINISTRATOR else 0
+            levels.events_default = 0
+            levels.state_default = 50 if ac.attributes == AccessControlMode.ADMINISTRATOR else 0
+            levels.events[EventType.ROOM_NAME] = levels.state_default
+            levels.events[EventType.ROOM_AVATAR] = levels.state_default
+            levels.events[EventType.ROOM_TOPIC] = levels.state_default
+        if self.main_intent.mxid not in levels.users:
+            levels.users[self.main_intent.mxid] = 9001 if is_initial else 100
+        return levels
+
     async def _create_matrix_room(self, source: 'u.User', info: ChatInfo) -> Optional[RoomID]:
     async def _create_matrix_room(self, source: 'u.User', info: ChatInfo) -> Optional[RoomID]:
         if self.mxid:
         if self.mxid:
             await self._update_matrix_room(source, info)
             await self._update_matrix_room(source, info)
@@ -809,6 +862,7 @@ class Portal(DBPortal, BasePortal):
         await self.update_info(source, info)
         await self.update_info(source, info)
         self.log.debug("Creating Matrix room")
         self.log.debug("Creating Matrix room")
         name: Optional[str] = None
         name: Optional[str] = None
+        power_levels = await self._get_power_levels(info=info, is_initial=True)
         initial_state = [{
         initial_state = [{
             "type": str(StateBridge),
             "type": str(StateBridge),
             "state_key": self.bridge_info_state_key,
             "state_key": self.bridge_info_state_key,
@@ -818,6 +872,9 @@ class Portal(DBPortal, BasePortal):
             "type": str(StateHalfShotBridge),
             "type": str(StateHalfShotBridge),
             "state_key": self.bridge_info_state_key,
             "state_key": self.bridge_info_state_key,
             "content": self.bridge_info,
             "content": self.bridge_info,
+        }, {
+            "type": str(EventType.ROOM_POWER_LEVELS),
+            "content": power_levels.serialize(),
         }]
         }]
         invites = [source.mxid]
         invites = [source.mxid]
         if self.config["bridge.encryption.default"] and self.matrix.e2ee:
         if self.config["bridge.encryption.default"] and self.matrix.e2ee:
@@ -842,12 +899,6 @@ class Portal(DBPortal, BasePortal):
                 "type": "m.room.related_groups",
                 "type": "m.room.related_groups",
                 "content": {"groups": [self.config["appservice.community_id"]]},
                 "content": {"groups": [self.config["appservice.community_id"]]},
             })
             })
-        if self.is_direct:
-            initial_state.append({
-                "type": str(EventType.ROOM_POWER_LEVELS),
-                "content": {"users": {self.main_intent.mxid: 100},
-                            "events": {"m.room.avatar": 0, "m.room.name": 0}}
-            })
 
 
         self.mxid = await self.main_intent.create_room(name=name, is_direct=self.is_direct,
         self.mxid = await self.main_intent.create_room(name=name, is_direct=self.is_direct,
                                                        initial_state=initial_state,
                                                        initial_state=initial_state,