From 882f9c13a16eb5aa577f4f52b5f01dae559a91c1 Mon Sep 17 00:00:00 2001 From: Malte E Date: Sat, 30 Jul 2022 22:25:15 +0200 Subject: [PATCH 1/2] bridge join via invite link to knock, bridge ban/unban (S->M) --- ROADMAP.md | 7 +-- mausignald/signald.py | 22 +++++++++ mautrix_signal/matrix.py | 38 +++++++++++++++ mautrix_signal/portal.py | 100 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 163 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 40444dae..6d02641c 100644 --- a/ROADMAP.md +++ b/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 diff --git a/mausignald/signald.py b/mausignald/signald.py index 878e84ac..25f4612c 100644 --- a/mausignald/signald.py +++ b/mausignald/signald.py @@ -369,6 +369,28 @@ async def unban_user(self, username: str, group_id: GroupID, users: list[Address ) 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, diff --git a/mautrix_signal/matrix.py b/mautrix_signal/matrix.py index 75f046ca..1ec09a7e 100644 --- a/mautrix_signal/matrix.py +++ b/mautrix_signal/matrix.py @@ -152,6 +152,44 @@ async def handle_ban( ) -> 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 diff --git a/mautrix_signal/portal.py b/mautrix_signal/portal.py index 58110d58..1a2e2d0c 100644 --- a/mautrix_signal/portal.py +++ b/mautrix_signal/portal.py @@ -1067,6 +1067,42 @@ async def handle_matrix_join_rules(self, sender: u.User, join_rule: JoinRule) -> 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 @@ async def _update_participants(self, source: u.User, info: ChatInfo) -> None: 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 @@ async def _update_participants(self, source: u.User, info: ChatInfo) -> None: 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(): From faffdf54ad381d15b37bdb1aac70b33f5fe58e97 Mon Sep 17 00:00:00 2001 From: Malte E Date: Wed, 22 Jun 2022 22:08:55 +0200 Subject: [PATCH 2/2] check requesting_members on failed accept/reject --- mautrix_signal/portal.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/mautrix_signal/portal.py b/mautrix_signal/portal.py index 1a2e2d0c..26d6cd59 100644 --- a/mautrix_signal/portal.py +++ b/mautrix_signal/portal.py @@ -1067,7 +1067,7 @@ async def handle_matrix_join_rules(self, sender: u.User, join_rule: JoinRule) -> 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: @@ -1077,7 +1077,9 @@ async def matrix_accept_knock(self, sender: u.User, user: p.Puppet | u.User) -> if isinstance(user, p.Puppet): await user.intent_for(self).ensure_joined(self.mxid) except RPCError as e: - raise RejectMatrixInvite(str(e)) from e + info = await self.signal.get_group(sender.username, self.chat_id) + if user.address in info.requesting_members: + 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: @@ -1099,11 +1101,13 @@ async def matrix_reject_knock(self, sender: u.User, user: p.Puppet | u.User) -> 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"]], - ) + info = await self.signal.get_group(sender.username, self.chat_id) + if user.address in info.requesting_members: + await user.intent_for(self).knock( + self.mxid, + reason=f"refusing membership failed: {e}", + servers=[self.config["homeserver.domain"]], + ) # endregion # region Signal event handling