diff --git a/mausignald/signald.py b/mausignald/signald.py index 7792c2a4..bae16f65 100644 --- a/mausignald/signald.py +++ b/mausignald/signald.py @@ -355,6 +355,7 @@ async def update_group( add_members: list[Address] | None = None, remove_members: list[Address] | None = None, update_access_control: GroupAccessControl | None = None, + update_role: GroupMember | None = None, ) -> Group | GroupV2 | None: update_params = { key: value @@ -370,6 +371,7 @@ async def update_group( "updateAccessControl": ( update_access_control.serialize() if update_access_control else None ), + "updateRole": (update_role.serialize() if update_role else None), }.items() if value is not None } diff --git a/mautrix_signal/matrix.py b/mautrix_signal/matrix.py index 4a45d9e4..8ab75ad1 100644 --- a/mautrix_signal/matrix.py +++ b/mautrix_signal/matrix.py @@ -188,7 +188,12 @@ async def handle_ephemeral_event( await super().handle_ephemeral_event(evt) async def handle_state_event(self, evt: StateEvent) -> None: - if evt.type not in (EventType.ROOM_NAME, EventType.ROOM_TOPIC, EventType.ROOM_AVATAR): + if evt.type not in ( + EventType.ROOM_NAME, + EventType.ROOM_TOPIC, + EventType.ROOM_AVATAR, + EventType.ROOM_POWER_LEVELS, + ): return user = await u.User.get_by_mxid(evt.sender) @@ -204,6 +209,8 @@ async def handle_state_event(self, evt: StateEvent) -> None: await portal.handle_matrix_avatar(user, evt.content.url) elif evt.type == EventType.ROOM_TOPIC: await portal.handle_matrix_topic(user, evt.content.topic) + elif evt.type == EventType.ROOM_POWER_LEVELS: + await portal.handle_matrix_power_level(user, evt.content, evt.unsigned.prev_content) async def allow_message(self, user: u.User) -> bool: return user.relay_whitelisted diff --git a/mautrix_signal/portal.py b/mautrix_signal/portal.py index 346178eb..7c354995 100644 --- a/mautrix_signal/portal.py +++ b/mautrix_signal/portal.py @@ -36,6 +36,7 @@ Group, GroupAccessControl, GroupID, + GroupMember, GroupMemberRole, GroupV2, GroupV2ID, @@ -774,6 +775,20 @@ async def handle_matrix_invite(self, invited_by: u.User, user: u.User | p.Puppet ) 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( + invited_by.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(invited_by.username, self.chat_id) + ) async def handle_matrix_name(self, user: u.User, name: str) -> None: if self.name == name or self.is_direct or not name: @@ -839,6 +854,101 @@ async def handle_matrix_avatar(self, user: u.User, url: ContentURI) -> None: except FileNotFoundError: pass + async def handle_matrix_power_level( + self, + sender: u.User, + levels: PowerLevelStateEventContent, + prev_content: PowerLevelStateEventContent | None = None, + ) -> None: + old_users = prev_content.users if prev_content else None + new_users = levels.users + changes = {} + sender, is_relay = await self.get_relay_sender(sender, "power level change") + if not sender: + return + + if not old_users: + changes = new_users + else: + for user, level in new_users.items(): + if ( + user + and user != self.main_intent.mxid + and (user not in old_users or level != old_users[user]) + ): + changes[user] = level + for user, level in old_users.items(): + if user and user != self.main_intent.mxid and user not in new_users: + changes[user] = levels.users_default + if changes: + for user, level in changes.items(): + address = p.Puppet.get_id_from_mxid(user) + if not address: + mx_user = await u.User.get_by_mxid(user, create=False) + if not mx_user or not mx_user.is_logged_in: + continue + address = mx_user.address + if not address or not address.uuid: + continue + signal_role = ( + GroupMemberRole.DEFAULT if level < 50 else GroupMemberRole.ADMINISTRATOR + ) + group_member = GroupMember(uuid=address.uuid, role=signal_role) + 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) + ) + return + if not prev_content or levels.invite != prev_content.invite: + try: + update_meta = await self.signal.update_group( + username=sender.username, + group_id=self.chat_id, + update_access_control=GroupAccessControl( + members=( + AccessControlMode.MEMBER + if levels.invite == 0 + else AccessControlMode.ADMINISTRATOR + ), + attributes=None, + link=None, + ), + ) + self.revision = update_meta.revision + except Exception as e: + self.log.exception(f"Failed to update Signal member add permission: {e}") + await self._update_power_levels( + await self.signal.get_group(sender.username, self.chat_id) + ) + return + if not prev_content or levels.state_default != prev_content.state_default: + try: + update_meta = await self.signal.update_group( + username=sender.username, + group_id=self.chat_id, + update_access_control=GroupAccessControl( + attributes=( + AccessControlMode.MEMBER + if levels.state_default == 0 + else AccessControlMode.ADMINISTRATOR + ), + members=None, + link=None, + ), + ) + self.revision = update_meta.revision + except Exception as e: + self.log.exception(f"Failed to update Signal metadata change permission: {e}") + await self._update_power_levels( + await self.signal.get_group(sender.username, self.chat_id) + ) + # endregion # region Signal event handling @@ -1335,33 +1445,7 @@ async def create_signal_group( pass if self.topic: await self.signal.update_group(source.username, self.chat_id, description=self.topic) - await self.signal.update_group( - username=source.username, - group_id=self.chat_id, - update_access_control=GroupAccessControl( - members=( - AccessControlMode.MEMBER - if levels.invite == 0 - else AccessControlMode.ADMINISTRATOR - ), - attributes=None, - link=None, - ), - ) - update_meta = await self.signal.update_group( - username=source.username, - group_id=self.chat_id, - update_access_control=GroupAccessControl( - attributes=( - AccessControlMode.MEMBER - if levels.state_default == 0 - else AccessControlMode.ADMINISTRATOR - ), - members=None, - link=None, - ), - ) - self.revision = update_meta.revision + await self.handle_matrix_power_level(source, levels) await self.update() await self.update_bridge_info()