Malte E пре 3 година
родитељ
комит
456513f689

+ 32 - 6
mausignald/types.py

@@ -197,9 +197,9 @@ class AnnouncementsMode(SerializableEnum):
 
 @dataclass
 class GroupAccessControl(SerializableAttrs):
-    attributes: Optional[AccessControlMode] = AccessControlMode.UNKNOWN
-    link: Optional[AccessControlMode] = AccessControlMode.UNKNOWN
-    members: Optional[AccessControlMode] = AccessControlMode.UNKNOWN
+    attributes: Optional[AccessControlMode] = None
+    link: Optional[AccessControlMode] = None
+    members: Optional[AccessControlMode] = None
 
 
 class GroupMemberRole(SerializableEnum):
@@ -222,9 +222,34 @@ class BannedGroupMember(SerializableAttrs):
     timestamp: int
 
 
+@dataclass
+class GroupChange(SerializableAttrs):
+    revision: int
+    editor: Address
+    delete_members: Optional[List[Address]] = None
+    delete_pending_members: Optional[List[Address]] = None
+    delete_requesting_members: Optional[List[Address]] = None
+    modified_profile_keys: Optional[List[GroupMember]] = None
+    modify_member_roles: Optional[List[GroupMember]] = None
+    new_access_control: Optional[GroupAccessControl] = None
+    new_avatar: bool = False
+    new_banned_members: Optional[List[GroupMember]] = None
+    new_description: Optional[str] = None
+    new_invite_link_password: bool = False
+    new_is_announcement_group: Optional[AnnouncementsMode] = None
+    new_members: Optional[List[GroupMember]] = None
+    new_pending_members: Optional[List[GroupMember]] = None
+    new_requesting_members: Optional[List[GroupMember]] = None
+    new_timer: Optional[int] = None
+    new_title: Optional[str] = None
+    new_unbanned_members: Optional[List[GroupMember]] = None
+    promote_pending_members: Optional[List[GroupMember]] = None
+    promote_requesting_members: Optional[List[GroupMember]] = None
+
+
 @dataclass(kw_only=True)
 class GroupV2(GroupV2ID, SerializableAttrs):
-    title: str
+    title: str = None
     description: Optional[str] = None
     avatar: Optional[str] = None
     timer: Optional[int] = None
@@ -233,7 +258,7 @@ class GroupV2(GroupV2ID, SerializableAttrs):
     access_control: GroupAccessControl = field(
         factory=lambda: GroupAccessControl(), json="accessControl"
     )
-    members: List[Address]
+    members: List[Address] = None
     member_detail: List[GroupMember] = field(factory=lambda: [], json="memberDetail")
     pending_members: List[Address] = field(factory=lambda: [], json="pendingMembers")
     pending_member_detail: List[GroupMember] = field(
@@ -242,6 +267,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
+    group_change: Optional[GroupChange] = None
 
 
 @dataclass
@@ -397,7 +423,7 @@ class MessageData(SerializableAttrs):
     contacts: List[SharedContact] = field(factory=lambda: [])
 
     group: Optional[Group] = None
-    group_v2: Optional[GroupV2ID] = field(default=None, json="groupV2")
+    group_v2: Optional[GroupV2] = field(default=None, json="groupV2")
 
     end_session: bool = field(default=False, json="endSession")
     expires_in_seconds: int = field(default=0, json="expiresInSeconds")

+ 1 - 0
mautrix_signal/config.py

@@ -68,6 +68,7 @@ class Config(BaseBridgeConfig):
         copy("bridge.autocreate_group_portal")
         copy("bridge.autocreate_contact_portal")
         copy("bridge.sync_with_custom_puppets")
+        copy("bridge.public_portals")
         copy("bridge.sync_direct_chat_list")
         copy("bridge.double_puppet_server_map")
         copy("bridge.double_puppet_allow_discovery")

+ 3 - 0
mautrix_signal/example-config.yaml

@@ -134,6 +134,9 @@ bridge:
     autocreate_group_portal: true
     # Whether or not to create portals for all contacts on login/connect.
     autocreate_contact_portal: false
+    # Whether or not to make portals of Signal groups in which joining via invite link does
+    # not need to be approved by an administrator publicly joinable on Matrix.
+    public_portals: true
     # Whether or not to use /sync to get read receipts and typing notifications
     # when double puppeting is enabled
     sync_with_custom_puppets: true

+ 214 - 2
mautrix_signal/portal.py

@@ -39,6 +39,7 @@ from mausignald.types import (
     Attachment,
     Group,
     GroupAccessControl,
+    GroupChange,
     GroupID,
     GroupMember,
     GroupMemberRole,
@@ -56,7 +57,7 @@ from mausignald.types import (
 )
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.bridge import BasePortal, RejectMatrixInvite, async_getter_lock
-from mautrix.errors import IntentError, MatrixError, MForbidden
+from mautrix.errors import IntentError, MatrixError, MBadState, MForbidden
 from mautrix.types import (
     AudioInfo,
     BeeperMessageStatusEventContent,
@@ -67,6 +68,7 @@ from mautrix.types import (
     EventType,
     FileInfo,
     ImageInfo,
+    JoinRule,
     MediaMessageEventContent,
     Membership,
     MessageEvent,
@@ -1215,6 +1217,216 @@ class Portal(DBPortal, BasePortal):
         self.log.debug(f"{user.mxid} was kicked by {sender.number} from {self.mxid}")
         await self._kick_with_puppet(user, sender)
 
+    async def handle_signal_group_change(self, group_change: GroupChange, source: u.User) -> None:
+        if self.revision < group_change.revision:
+            self.revision = group_change.revision
+        else:
+            return
+        editor = await p.Puppet.get_by_address(group_change.editor)
+        editor_intent = editor.intent_for(self)
+        if (
+            group_change.delete_members
+            or group_change.delete_pending_members
+            or group_change.delete_requesting_members
+        ):
+            for address in (
+                (group_change.delete_members or [])
+                + (group_change.delete_pending_members or [])
+                + (group_change.delete_requesting_members or [])
+            ):
+                user = await p.Puppet.get_by_address(address)
+                if not user:
+                    continue
+                if user == editor:
+                    await editor_intent.leave_room(self.mxid)
+                else:
+                    try:
+                        await editor_intent.kick_user(self.mxid, user.mxid)
+                    except MForbidden:
+                        try:
+                            await self.main_intent.kick_user(
+                                self.mxid, user.mxid, reason=f"removed by {editor.name}"
+                            )
+                        except MForbidden as e:
+                            self.log.debug(f"Could not remove {user.mxid}: {e}")
+                    except MBadState as e:
+                        self.log.debug(f"Could not remove {user.mxid}: {e}")
+        if group_change.modify_member_roles:
+            levels = await editor.intent_for(self).get_power_levels(self.mxid)
+            for group_member in group_change.modify_member_roles:
+                user = await p.Puppet.get_by_address(Address(uuid=group_member.uuid))
+                if not user:
+                    continue
+                if group_member.role == GroupMemberRole.ADMINISTRATOR:
+                    new_pl = 50
+                else:
+                    new_pl = 0
+                levels.users[user.mxid] = new_pl
+            await self._try_with_puppet(
+                lambda i: i.set_power_levels(self.mxid, levels), puppet=editor
+            )
+
+        if group_change.new_banned_members:
+            for banned_member in group_change.new_banned_members:
+                user = await p.Puppet.get_by_address(Address(uuid=banned_member.uuid))
+                if not user:
+                    continue
+                try:
+                    await editor_intent.ban_user(self.mxid, user.mxid)
+                except MForbidden:
+                    try:
+                        await self.main_intent.ban_user(
+                            self.mxid, user.mxid, reason=f"banned by {editor.name}"
+                        )
+                    except MForbidden as e:
+                        self.log.debug(f"Could not ban {user.mxid}: {e}")
+                except MBadState as e:
+                    self.log.debug(f"Could not ban {user.mxid}: {e}")
+        if group_change.new_unbanned_members:
+            for banned_member in group_change.new_unbanned_members:
+                user = await p.Puppet.get_by_address(Address(uuid=banned_member.uuid))
+                if not user:
+                    continue
+                try:
+                    await editor_intent.unban_user(self.mxid, user.mxid)
+                except MForbidden:
+                    try:
+                        await self.main_intent.unban_user(
+                            self.mxid, user.mxid, reason=f"unbanned by {editor.name}"
+                        )
+                    except MForbidden as e:
+                        self.log.debug(f"Could not unban {user.mxid}: {e}")
+                except MBadState as e:
+                    self.log.debug(f"Could not unban {user.mxid}: {e}")
+        if (
+            group_change.new_members
+            or group_change.new_pending_members
+            or group_change.promote_requesting_members
+        ):
+            banned_users = await self.az.intent.get_room_members(self.mxid, (Membership.BAN,))
+            for group_member in (
+                (group_change.new_members or [])
+                + (group_change.new_pending_members or [])
+                + (group_change.promote_requesting_members or [])
+            ):
+                user = await p.Puppet.get_by_address(Address(uuid=group_member.uuid))
+                if not user:
+                    continue
+                try:
+                    await source.sync_contact(Address(uuid=group_member.uuid))
+                except ProfileUnavailableError:
+                    self.log.debug(
+                        f"Profile of puppet with uuid {group_member.uuid} is unavailable"
+                    )
+                if user.mxid in banned_users:
+                    await self._try_with_puppet(
+                        lambda i: i.unban_user(self.mxid, user.mxid), puppet=editor
+                    )
+                try:
+                    await editor_intent.invite_user(self.mxid, user.mxid, check_cache=True)
+                except (MForbidden, IntentError):
+                    try:
+                        await self.main_intent.invite_user(
+                            self.mxid,
+                            user.mxid,
+                            reason=f"invited by {editor.name}",
+                            check_cache=True,
+                        )
+                    except (MForbidden, IntentError) as e:
+                        self.log.debug(f"{editor.name} could not invite {user.mxid}: {e}")
+                except MBadState as e:
+                    self.log.debug(f"{editor.name} could not invite {user.mxid}: {e}")
+                if (
+                    group_member
+                    in (group_change.new_members or [])
+                    + (group_change.promote_requesting_members or [])
+                    and not user.is_real_user()
+                ):
+                    try:
+                        await user.intent_for(self).ensure_joined(self.mxid)
+                    except IntentError as e:
+                        self.log.debug(f"{user.name} could not join group: {e}")
+        if group_change.promote_pending_members:
+            for member in group_change.promote_pending_members:
+                user = await p.Puppet.get_by_address(Address(uuid=member.uuid))
+                if not user:
+                    continue
+                try:
+                    await source.sync_contact(Address(uuid=group_member.uuid))
+                except ProfileUnavailableError:
+                    self.log.debug(
+                        f"Profile of puppet with uuid {group_member.uuid} is unavailable"
+                    )
+                try:
+                    user.intent_for(self).ensure_joined(self.mxid)
+                except IntentError as e:
+                    self.log.debug(f"{user.name} could not join group: {e}")
+        if group_change.new_requesting_members:
+            for member in group_change.new_requesting_members:
+                user = await p.Puppet.get_by_address(Address(uuid=member.uuid))
+                if not user:
+                    continue
+                try:
+                    await source.sync_contact(Address(uuid=group_member.uuid))
+                except ProfileUnavailableError:
+                    self.log.debug(
+                        f"Profile of puppet with uuid {group_member.uuid} is unavailable"
+                    )
+                try:
+                    await user.intent_for(self).knock(self.mxid, reason="via invite link")
+                except (MForbidden, MBadState) as e:
+                    self.log.debug(f"{user.name} failed knock: {e}")
+        if group_change.new_access_control:
+            ac = group_change.new_access_control
+            if ac.attributes or ac.members:
+                levels = await editor.intent_for(self).get_power_levels(self.mxid)
+                if ac.attributes:
+                    meta_edit_level = 50 if ac.attributes == AccessControlMode.ADMINISTRATOR else 0
+                    levels.events[EventType.ROOM_NAME] = meta_edit_level
+                    levels.events[EventType.ROOM_AVATAR] = meta_edit_level
+                    levels.events[EventType.ROOM_TOPIC] = meta_edit_level
+                if ac.members:
+                    levels.invite = 50 if ac.members == AccessControlMode.ADMINISTRATOR else 0
+                await self._try_with_puppet(
+                    lambda i: i.set_power_levels(self.mxid, levels), puppet=editor
+                )
+            if ac.link:
+                join_rule = JoinRule.INVITE
+                if ac.link == AccessControlMode.ANY and self.config["bridge.public_portals"]:
+                    join_rule = JoinRule.PUBLIC
+                elif ac.link == AccessControlMode.ADMINISTRATOR:
+                    join_rule = JoinRule.KNOCK
+                await self._try_with_puppet(
+                    lambda i: i.set_join_rule(self.mxid, join_rule), puppet=editor
+                )
+        if group_change.new_is_announcement_group:
+            levels = await editor.intent_for(self).get_power_levels(self.mxid)
+            if group_change.new_is_announcement_group == AnnouncementsMode.ENABLED:
+                levels.events_default = 50
+            elif group_change.new_is_announcement_group == AnnouncementsMode.DISABLED:
+                levels.events_default = 0
+            await self._try_with_puppet(
+                lambda i: i.set_power_levels(self.mxid, levels), puppet=editor
+            )
+        changed = False
+        if group_change.new_description:
+            changed = await self._update_topic(group_change.new_description, editor)
+        if group_change.new_title:
+            changed = await self._update_name(group_change.new_title, editor) or changed
+        if group_change.new_avatar:
+            changed = (
+                await self._update_avatar(
+                    await self.signal.get_group(
+                        source.username, self.chat_id, group_change.revision
+                    ),
+                    editor,
+                )
+                or changed
+            )
+        if changed:
+            await self.update_bridge_info()
+            await self.update()
+
     @staticmethod
     async def _make_media_content(
         attachment: Attachment, data: bytes
@@ -1525,7 +1737,7 @@ class Portal(DBPortal, BasePortal):
                 self.name = puppet.name
             return
 
-        if isinstance(info, GroupV2ID):
+        if not info.title:
             try:
                 info = await self.signal.get_group(source.username, info.id, info.revision or -1)
             except Exception as e:

+ 7 - 0
mautrix_signal/signal.py

@@ -217,6 +217,13 @@ class SignalHandler(SignaldClient):
                     f"Failed to create room for incoming message {msg.timestamp}, dropping message"
                 )
                 return
+        elif (
+            msg.group_v2
+            and msg.group_v2.group_change
+            and msg.group_v2.revision == portal.revision + 1
+        ):
+            self.log.debug(f"Got GroupChange for {msg.group_v2.id}, updating info")
+            await portal.handle_signal_group_change(msg.group_v2.group_change, user)
         elif msg.group_v2 and msg.group_v2.revision > portal.revision:
             self.log.debug(f"Got new revision of {msg.group_v2.id}, updating info")
             await portal.update_info(user, msg.group_v2, sender)