Răsfoiți Sursa

Add support for Matrix->Signal group name/avatar changes

Tulir Asokan 4 ani în urmă
părinte
comite
a419d8eb57
4 a modificat fișierele cu 81 adăugiri și 5 ștergeri
  1. 5 5
      ROADMAP.md
  2. 22 0
      mausignald/signald.py
  3. 16 0
      mautrix_signal/matrix.py
  4. 38 0
      mautrix_signal/portal.py

+ 5 - 5
ROADMAP.md

@@ -14,10 +14,10 @@
       * [ ] Stickers
   * [x] Message reactions
   * [ ] Message redactions
-  * [ ] Group info changes
-    * [ ] Name
-    * [ ] Avatar
-  * [ ] Typing notifications
+  * [x] Group info changes
+    * [x] Name
+    * [x] Avatar
+  * [ ] Typing notifications
   * [ ] Read receipts (currently partial support, only marks last message)
   * [x] Delivery receipts (sent after message is bridged)
 * Signal → Matrix
@@ -38,6 +38,7 @@
   * [ ] Profile info changes
     * [x] When restarting bridge or syncing
     * [ ] Real time
+  * [ ] Group permissions
   * [x] Typing notifications
   * [x] Read receipts
   * [ ] Delivery receipts (there's no good way to bridge these)
@@ -46,7 +47,6 @@
   * [x] Automatic portal creation
     * [x] At startup
     * [x] When receiving message
-      * [ ] in v2 groups
   * [ ] Provisioning API for logging in
     * [x] Linking as secondary device
     * [ ] Registering as primary device

+ 22 - 0
mausignald/signald.py

@@ -174,6 +174,28 @@ class SignaldClient(SignaldRPCClient):
         return ([Group.deserialize(group) for group in resp["groups"]]
                 + [GroupV2.deserialize(group) for group in resp["groupsv2"]])
 
+    async def update_group(self, username: str, group_id: GroupID, title: Optional[str] = None,
+                           avatar_path: Optional[str] = None,
+                           add_members: Optional[List[Address]] = None,
+                           remove_members: Optional[List[Address]] = None
+                           ) -> Union[Group, GroupV2, None]:
+        update_params = {key: value for key, value in {
+            "groupID": group_id,
+            "avatar": avatar_path,
+            "title": title,
+            "addMembers": [addr.serialize() for addr in add_members] if add_members else None,
+            "removeMembers": ([addr.serialize() for addr in remove_members]
+                              if remove_members else None),
+        }.items() if value is not None}
+        resp = await self.request("update_group", "update_group", version="v1", account=username,
+                                  **update_params)
+        if "v1" in resp:
+            return Group.deserialize(resp["v1"])
+        elif "v2" in resp:
+            return GroupV2.deserialize(resp["v2"])
+        else:
+            return None
+
     async def get_group(self, username: str, group_id: GroupID, revision: int = -1
                         ) -> Optional[GroupV2]:
         resp = await self.request("get_group", "get_group", account=username, groupID=group_id,

+ 16 - 0
mautrix_signal/matrix.py

@@ -135,3 +135,19 @@ class MatrixHandler(BaseMatrixHandler):
             await self.handle_typing(evt.room_id, evt.content.user_ids)
         else:
             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_AVATAR):
+            return
+
+        user = await u.User.get_by_mxid(evt.sender)
+        if not user:
+            return
+        portal = await po.Portal.get_by_mxid(evt.room_id)
+        if not portal:
+            return
+
+        if evt.type == EventType.ROOM_NAME:
+            await portal.handle_matrix_name(user, evt.content.name)
+        elif evt.type == EventType.ROOM_AVATAR:
+            await portal.handle_matrix_avatar(user, evt.content.url)

+ 38 - 0
mautrix_signal/portal.py

@@ -277,6 +277,44 @@ class Portal(DBPortal, BasePortal):
             self.log.debug(f"{user.mxid} left portal to {self.chat_id}")
             # TODO cleanup if empty
 
+    async def handle_matrix_name(self, user: 'u.User', name: str) -> None:
+        if self.name == name or self.is_direct:
+            return
+        self.name = name
+        self.log.debug(f"{user.mxid} changed the group name, sending to Signal")
+        try:
+            await self.signal.update_group(user.username, self.chat_id, title=name)
+        except Exception:
+            self.log.exception("Failed to update Signal group name")
+            self.name = None
+
+    async def handle_matrix_avatar(self, user: 'u.User', url: ContentURI) -> None:
+        if self.is_direct:
+            return
+
+        data = await self.main_intent.download_media(url)
+        new_hash = hashlib.sha256(data).hexdigest()
+        if new_hash == self.avatar_hash:
+            self.log.debug(f"New avatar from Matrix set by {user.mxid} is same as current one")
+            return
+        self.avatar_url = url
+        self.avatar_hash = new_hash
+        path = os.path.join(self.config["signal.outgoing_attachment_dir"],
+                            f"mautrix-signal-avatar-{str(uuid4())}")
+        self.log.debug(f"{user.mxid} changed the group avatar, sending to Signal")
+        try:
+            with open(path, "wb") as file:
+                file.write(data)
+            await self.signal.update_group(user.username, self.chat_id, avatar_path=path)
+        except Exception:
+            self.log.exception("Failed to update Signal group avatar")
+            self.avatar_hash = None
+        if self.config["signal.remove_file_after_handling"]:
+            try:
+                os.remove(path)
+            except FileNotFoundError:
+                pass
+
     # endregion
     # region Signal event handling