Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bridge join via invite link to knock (Signal->Matrix), bridge ban/unban (Signal-> Matrix) #275

Merged
merged 2 commits into from
Dec 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions mausignald/signald.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 38 additions & 0 deletions mautrix_signal/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 103 additions & 1 deletion mautrix_signal/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,46 @@ async def handle_matrix_join_rules(self, sender: u.User, join_rule: JoinRule) ->
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:
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:
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:
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

Expand Down Expand Up @@ -2020,12 +2060,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:
Expand Down Expand Up @@ -2053,6 +2142,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():
Expand Down