Browse Source

Add support for Matrix->Signal files

Tulir Asokan 4 năm trước cách đây
mục cha
commit
43abdf1dec
5 tập tin đã thay đổi với 65 bổ sung19 xóa
  1. 1 1
      ROADMAP.md
  2. 6 7
      mausignald/types.py
  3. 2 0
      mautrix_signal/config.py
  4. 6 0
      mautrix_signal/example-config.yaml
  5. 50 11
      mautrix_signal/portal.py

+ 1 - 1
ROADMAP.md

@@ -6,7 +6,7 @@
     * [ ] ‡Formatting
     * [ ] Media
       * [ ] Images
-      * [ ] Files
+      * [x] Files
       * [ ] Gifs
       * [ ] Locations
       * [ ] Stickers

+ 6 - 7
mausignald/types.py

@@ -68,21 +68,20 @@ class FullGroup(Group, SerializableAttrs['FullGroup']):
 class Attachment(SerializableAttrs['Attachment']):
     width: int = 0
     height: int = 0
+    caption: Optional[str] = None
+    preview: Optional[str] = None
+    blurhash: Optional[str] = None
     voice_note: bool = attr.ib(default=False, metadata={"json": "voiceNote"})
     content_type: Optional[str] = attr.ib(default=None, metadata={"json": "contentType"})
+    custom_filename: Optional[str] = attr.ib(default=None, metadata={"json": "customFilename"})
 
     # Only for incoming
     id: Optional[str] = None
-    stored_filename: Optional[str] = attr.ib(default=None, metadata={"json": "storedFilename"})
-
-    blurhash: Optional[str] = None
+    incoming_filename: Optional[str] = attr.ib(default=None, metadata={"json": "storedFilename"})
     digest: Optional[str] = None
 
     # Only for outgoing
-    filename: Optional[str] = None
-
-    caption: Optional[str] = None
-    preview: Optional[str] = None
+    outgoing_filename: Optional[str] = attr.ib(default=None, metadata={"json": "filename"})
 
 
 @dataclass

+ 2 - 0
mautrix_signal/config.py

@@ -54,6 +54,8 @@ class Config(BaseBridgeConfig):
         copy("appservice.community_id")
 
         copy("signal.socket_path")
+        copy("signal.outgoing_attachment_dir")
+        copy("signal.remove_file_after_handling")
 
         copy("metrics.enabled")
         copy("metrics.listen_port")

+ 6 - 0
mautrix_signal/example-config.yaml

@@ -66,6 +66,12 @@ metrics:
 signal:
     # Path to signald unix socket
     socket_path: /var/run/signald/signald.sock
+    # Directory for temp files when sending files to Signal. This should be an
+    # absolute path that signald can read. For attachments in the other direction,
+    # make sure signald is configured to use an absolute path as the data directory.
+    outgoing_attachment_dir: /tmp
+    # Whether or not message attachments should be removed from disk after they're bridged.
+    remove_file_after_handling: true
 
 # Bridge config
 bridge:

+ 50 - 11
mautrix_signal/portal.py

@@ -16,10 +16,12 @@
 from typing import (Dict, Tuple, Optional, List, Deque, Set, Any, Union, AsyncGenerator,
                     Awaitable, TYPE_CHECKING, cast)
 from collections import deque
-from uuid import UUID
+from uuid import UUID, uuid4
 import mimetypes
 import asyncio
+import os.path
 import time
+import os
 
 from mausignald.types import (Address, MessageData, Reaction, Quote, FullGroup, Group, Contact,
                               Profile, Attachment)
@@ -134,6 +136,29 @@ class Portal(DBPortal, BasePortal):
     # endregion
     # region Matrix event handling
 
+    @staticmethod
+    def _make_attachment(message: MediaMessageEventContent, path: str) -> Attachment:
+        attachment = Attachment(custom_filename=message.body, content_type=message.info.mimetype,
+                                outgoing_filename=path)
+        info = message.info
+        attachment.width = info.get("w", info.get("width", 0))
+        attachment.height = info.get("h", info.get("height", 0))
+        attachment.voice_note = message.msgtype == MessageType.AUDIO
+        return attachment
+
+    async def _download_matrix_media(self, message: MediaMessageEventContent) -> str:
+        if message.file:
+            data = await self.main_intent.download_media(message.file.url)
+            data = decrypt_attachment(data, message.file.key.key,
+                                      message.file.hashes.get("sha256"), message.file.iv)
+        else:
+            data = await self.main_intent.download_media(message.url)
+        path = os.path.join(self.config["signal.outgoing_attachment_dir"],
+                            f"mautrix-signal-{str(uuid4())}")
+        with open(path, "wb") as file:
+            file.write(data)
+        return path
+
     async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent,
                                     event_id: EventID) -> None:
         if ((message.get(self.bridge.real_user_content_key, False)
@@ -150,18 +175,28 @@ class Portal(DBPortal, BasePortal):
             quote = Quote(id=reply.timestamp, author=Address(uuid=reply.sender), text="")
 
         text = message.body
+        attachments: Optional[List[Attachment]] = None
+        attachment_path: Optional[str] = None
         if message.msgtype == MessageType.EMOTE:
             text = f"/me {text}"
         elif message.msgtype.is_media:
-            # TODO media support
-            return
-        await self.signal.send(username=sender.username, recipient=self.recipient,
-                               body=text, quote=quote, timestamp=request_id)
+            attachment_path = await self._download_matrix_media(message)
+            attachment = self._make_attachment(message, attachment_path)
+            attachments = [attachment]
+            text = None
+            self.log.trace("Formed outgoing attachment %s", attachment)
+        await self.signal.send(username=sender.username, recipient=self.recipient, body=text,
+                               quote=quote, attachments=attachments, timestamp=request_id)
         msg = DBMessage(mxid=event_id, mx_room=self.mxid, sender=sender.uuid, timestamp=request_id,
                         signal_chat_id=self.chat_id, signal_receiver=self.receiver)
         await msg.insert()
         await self._send_delivery_receipt(event_id)
         self.log.debug(f"Handled Matrix message {event_id} -> {request_id}")
+        if attachment_path and self.config["signal.remove_file_after_handling"]:
+            try:
+                os.remove(attachment_path)
+            except FileNotFoundError:
+                pass
 
     async def handle_matrix_reaction(self, sender: 'u.User', event_id: EventID,
                                      reacting_to: EventID, emoji: str) -> None:
@@ -310,21 +345,25 @@ class Portal(DBPortal, BasePortal):
         else:
             msgtype = MessageType.FILE
             info = FileInfo(mimetype=attachment.content_type)
-        # TODO add something to signald so we can get the actual file name if one is set
-        ext = mimetypes.guess_extension(attachment.content_type) or ""
-        return MediaMessageEventContent(msgtype=msgtype, body=attachment.id + ext, info=info)
+        if not attachment.custom_filename:
+            ext = mimetypes.guess_extension(attachment.content_type) or ""
+            attachment.custom_filename = attachment.id + ext
+        return MediaMessageEventContent(msgtype=msgtype, info=info,
+                                        body=attachment.custom_filename)
 
     async def _handle_signal_attachment(self, intent: IntentAPI, attachment: Attachment
                                         ) -> Optional[MediaMessageEventContent]:
         self.log.trace(f"Reuploading attachment {attachment}")
         if not attachment.content_type:
-            attachment.content_type = (magic.from_file(attachment.stored_filename, mime=True)
+            attachment.content_type = (magic.from_file(attachment.incoming_filename, mime=True)
                                        if magic is not None else "application/octet-stream")
 
         content = self._make_media_content(attachment)
 
-        with open(attachment.stored_filename, "rb") as file:
+        with open(attachment.incoming_filename, "rb") as file:
             data = file.read()
+        if self.config["signal.remove_file_after_handling"]:
+            os.remove(attachment.incoming_filename)
 
         upload_mime_type = attachment.content_type
         if self.encrypted and encrypt_attachment:
@@ -332,7 +371,7 @@ class Portal(DBPortal, BasePortal):
             upload_mime_type = "application/octet-stream"
 
         content.url = await intent.upload_media(data, mime_type=upload_mime_type,
-                                                filename=content.body)
+                                                filename=attachment.id)
         if content.file:
             content.file.url = content.url
             content.url = None