瀏覽代碼

Add support for incoming contact messages

Tulir Asokan 3 年之前
父節點
當前提交
6b8c4ca6ec
共有 5 個文件被更改,包括 96 次插入5 次删除
  1. 1 0
      CHANGELOG.md
  2. 2 2
      ROADMAP.md
  3. 68 1
      mausignald/types.py
  4. 23 0
      mautrix_signal/portal.py
  5. 2 2
      mautrix_signal/signal.py

+ 1 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@ Target signald version: [v0.16.0](https://gitlab.com/signald/signald/-/releases/
     to enable the `enable_disappearing_messages_in_groups` config option.
 * Notifications about incoming calls.
 * Support for voice messages with [MSC3245].
+* Support for incoming contact share messages.
 
 ### Improved
 * Formatted all code using [black](https://github.com/psf/black)

+ 2 - 2
ROADMAP.md

@@ -29,12 +29,12 @@
   * [ ] Message content
     * [x] Text
     * [x] Mentions
-    * [ ] Media
+    * [x] Media
       * [x] Images
       * [x] Voice notes
       * [x] Files
       * [x] Gifs
-      * [ ] Contacts
+      * [x] Contacts
       * [x] Locations
       * [x] Stickers
   * [x] Message reactions

+ 68 - 1
mausignald/types.py

@@ -277,9 +277,72 @@ class RemoteDelete(SerializableAttrs):
     target_sent_timestamp: int = field(json="targetSentTimestamp")
 
 
+class SharedContactDetailType(SerializableEnum):
+    HOME = "HOME"
+    WORK = "WORK"
+    MOBILE = "MOBILE"
+    CUSTOM = "CUSTOM"
+
+
+@dataclass
+class SharedContactDetail(SerializableAttrs):
+    type: SharedContactDetailType
+    value: str
+    label: Optional[str] = None
+
+    @property
+    def type_or_label(self) -> str:
+        if self.type != SharedContactDetailType.CUSTOM:
+            return self.type.value.title()
+        return self.label
+
+
+@dataclass
+class SharedContactAvatar(SerializableAttrs):
+    attachment: Attachment
+    is_profile: bool
+
+
+@dataclass
+class SharedContactName(SerializableAttrs):
+    display: Optional[str] = None
+    given: Optional[str] = None
+    middle: Optional[str] = None
+    family: Optional[str] = None
+    prefix: Optional[str] = None
+    suffix: Optional[str] = None
+
+    @property
+    def parts(self) -> List[str]:
+        return [self.prefix, self.given, self.middle, self.family, self.suffix]
+
+    def __str__(self) -> str:
+        if self.display:
+            return self.display
+        return " ".join(part for part in self.parts if part)
+
+
+@dataclass
+class SharedContactAddress(SerializableAttrs):
+    type: SharedContactDetailType
+    label: Optional[str] = None
+    street: Optional[str] = None
+    pobox: Optional[str] = None
+    neighborhood: Optional[str] = None
+    city: Optional[str] = None
+    region: Optional[str] = None
+    postcode: Optional[str] = None
+    country: Optional[str] = None
+
+
 @dataclass
 class SharedContact(SerializableAttrs):
-    pass
+    name: SharedContactName
+    organization: Optional[str] = None
+    avatar: Optional[SharedContactAvatar] = None
+    email: List[SharedContactDetail] = field(factory=lambda: [])
+    phone: List[SharedContactDetail] = field(factory=lambda: [])
+    address: Optional[SharedContactAddress] = None
 
 
 @dataclass
@@ -304,6 +367,10 @@ class MessageData(SerializableAttrs):
 
     remote_delete: Optional[RemoteDelete] = field(default=None, json="remoteDelete")
 
+    @property
+    def is_message(self) -> bool:
+        return bool(self.body or self.attachments or self.sticker or self.contacts)
+
 
 @dataclass
 class SentSyncMessage(SerializableAttrs):

+ 23 - 0
mautrix_signal/portal.py

@@ -44,6 +44,7 @@ from mausignald.types import (
     Quote,
     QuotedAttachment,
     Reaction,
+    SharedContact,
     Sticker,
 )
 from mautrix.appservice import AppService, IntentAPI
@@ -794,6 +795,13 @@ class Portal(DBPortal, BasePortal):
                     intent, content, timestamp=message.timestamp, event_type=EventType.STICKER
                 )
 
+        for contact in message.contacts:
+            content = await self._handle_signal_contact(contact)
+            if reply_to and not message.body:
+                content.set_reply(reply_to)
+                reply_to = None
+            event_id = await self._send_message(intent, content, timestamp=message.timestamp)
+
         for attachment in message.attachments:
             if not attachment.incoming_filename:
                 self.log.warning(
@@ -919,6 +927,21 @@ class Portal(DBPortal, BasePortal):
         await self._upload_attachment(intent, content, data, attachment.id)
         return content
 
+    @staticmethod
+    async def _handle_signal_contact(contact: SharedContact) -> TextMessageEventContent:
+        msg = f"Shared contact: {contact.name!s}"
+        if contact.phone:
+            msg += "\n"
+            for phone in contact.phone:
+                msg += f"\nPhone: {phone.value} ({phone.type_or_label})"
+        if contact.email:
+            msg += "\n"
+            for email in contact.email:
+                msg += f"\nEmail: {email.value} ({email.type_or_label})"
+        content = TextMessageEventContent(msgtype=MessageType.TEXT, body=msg)
+        content["fi.mau.signal.contact"] = contact.serialize()
+        return content
+
     async def _add_sticker_meta(self, sticker: Sticker, content: MediaMessageEventContent) -> None:
         try:
             pack = self._sticker_meta_cache[sticker.pack_id]

+ 2 - 2
mautrix_signal/signal.py

@@ -123,7 +123,7 @@ class SignalHandler(SignaldClient):
                 return
         assert portal
         if not portal.mxid:
-            if not msg.body and not msg.attachments and not msg.sticker and not msg.group_v2:
+            if not msg.is_message and not msg.group_v2:
                 user.log.debug(
                     f"Ignoring message {msg.timestamp},"
                     " probably not bridgeable as there's no portal yet"
@@ -142,7 +142,7 @@ class SignalHandler(SignaldClient):
             await portal.update_info(user, msg.group_v2, sender)
         if msg.reaction:
             await portal.handle_signal_reaction(sender, msg.reaction, msg.timestamp)
-        if msg.body or msg.attachments or msg.sticker:
+        if msg.is_message:
             await portal.handle_signal_message(user, sender, msg)
             if msg.expires_in_seconds is not None:
                 await portal.update_expires_in_seconds(sender, msg.expires_in_seconds)