Browse Source

Handle more xma share types

Tulir Asokan 2 năm trước cách đây
mục cha
commit
f73c461817
2 tập tin đã thay đổi với 92 bổ sung22 xóa
  1. 12 2
      mauigpapi/types/thread_item.py
  2. 80 20
      mautrix_instagram/portal.py

+ 12 - 2
mauigpapi/types/thread_item.py

@@ -78,6 +78,9 @@ class ThreadItemType(ExtensibleEnum):
     CLIP = "clip"
     GUIDE_SHARE = "guide_share"
     XMA_MEDIA_SHARE = "xma_media_share"
+    XMA_REEL_SHARE = "xma_reel_share"
+    XMA_STORY_SHARE = "xma_story_share"
+    XMA_REEL_MENTION = "xma_reel_mention"
 
 
 @dataclass(kw_only=True)
@@ -375,9 +378,10 @@ class AnimatedMediaImages(SerializableAttrs):
 @dataclass(kw_only=True)
 class AnimatedMediaItem(SerializableAttrs):
     id: str
-    is_random: str
-    is_sticker: str
     images: AnimatedMediaImages
+    # user: {is_verified: bool, username: str}
+    # is_random: str | None
+    # is_sticker: str | bool
 
 
 class ReactionType(SerializableEnum):
@@ -540,6 +544,9 @@ class ThreadItem(SerializableAttrs):
     client_context: Optional[str] = None
     show_forward_attribution: Optional[bool] = None
     action_log: Optional[ThreadItemActionLog] = None
+    auxiliary_text: Optional[str] = None
+    auxiliary_text_source_type: Optional[int] = None
+    message_item_type: Optional[str] = None
 
     replied_to_message: Optional["ThreadItem"] = None
 
@@ -550,6 +557,9 @@ class ThreadItem(SerializableAttrs):
     media_share: Optional[MediaShareItem] = None
     direct_media_share: Optional[DirectMediaShareItem] = None
     xma_media_share: Optional[List[XMAMediaShareItem]] = None
+    xma_story_share: Optional[List[XMAMediaShareItem]] = None
+    xma_reel_share: Optional[List[XMAMediaShareItem]] = None
+    xma_reel_mention: Optional[List[XMAMediaShareItem]] = None
     reel_share: Optional[ReelShareItem] = None
     story_share: Optional[StoryShareItem] = None
     location: Optional[Location] = None

+ 80 - 20
mautrix_instagram/portal.py

@@ -806,6 +806,7 @@ class Portal(DBPortal, BasePortal):
         info: ImageInfo | VideoInfo | AudioInfo,
         intent: IntentAPI,
         convert_fn: Callable[[bytes, str], Awaitable[tuple[bytes, str]]] | None = None,
+        allow_encrypt: bool = True,
     ) -> MediaMessageEventContent:
         async with source.client.raw_http_get(url) as resp:
             try:
@@ -843,7 +844,7 @@ class Portal(DBPortal, BasePortal):
         upload_mime_type = info.mimetype
         upload_file_name = file_name
         decryption_info = None
-        if self.encrypted and encrypt_attachment:
+        if allow_encrypt and self.encrypted and encrypt_attachment:
             data, decryption_info = encrypt_attachment(data)
             upload_mime_type = "application/octet-stream"
             upload_file_name = None
@@ -881,8 +882,18 @@ class Portal(DBPortal, BasePortal):
         elif item.voice_media:
             media_data = item.voice_media
             method = self._reupload_instagram_voice
-        elif item.xma_media_share:
-            media_data = item.xma_media_share[0]
+        elif (
+            item.xma_media_share
+            or item.xma_story_share
+            or item.xma_reel_share
+            or item.xma_reel_mention
+        ):
+            media_data = (
+                item.xma_media_share
+                or item.xma_story_share
+                or item.xma_reel_share
+                or item.xma_reel_mention
+            )[0]
             method = self._reupload_instagram_xma
         elif item.reel_share:
             media_data = item.reel_share.media
@@ -924,6 +935,7 @@ class Portal(DBPortal, BasePortal):
         await self._add_instagram_reply(content, item.replied_to_message)
         return content
 
+    # TODO this might be unused
     async def _handle_instagram_media_share(
         self, source: u.User, intent: IntentAPI, item: ThreadItem
     ) -> EventID | None:
@@ -1032,26 +1044,66 @@ class Portal(DBPortal, BasePortal):
         self, source: u.User, intent: IntentAPI, item: ThreadItem
     ) -> EventID | None:
         # N.B. _get_instagram_media_info also only supports downloading the first xma item
-        media = item.xma_media_share[0]
-        if len(item.xma_media_share) != 1:
+        xma_list = (
+            item.xma_media_share
+            or item.xma_story_share
+            or item.xma_reel_share
+            or item.xma_reel_mention
+        )
+        media = xma_list[0]
+        if len(xma_list) != 1:
             self.log.warning(f"Item {item.item_id} has multiple xma media share parts")
-        if media.xma_layout_type != 0:
+        if media.xma_layout_type not in (0, 4):
             self.log.warning(f"Unrecognized xma layout type {media.xma_layout_type}")
         content = await self._convert_instagram_media(source, intent, item)
 
-        caption_body = f"> {media.title_text}\n\n" f"{media.target_url}"
-        target_url_pretty = str(URL(media.target_url).with_query(None)).replace("https://www.", "")
-        escaped_title_text = html.escape(media.title_text)
-        escaped_header_text = html.escape(media.header_title_text)
-        if escaped_title_text.startswith(escaped_header_text):
-            escaped_title_text = (
+        # Post shares (layout type 0): media title text
+        # Reel shares/replies/reactions (layout type 4): item text
+        caption_text = media.title_text or item.text or ""
+        caption_body = (
+            f"> {caption_text}\n\n{media.target_url}" if caption_text else media.target_url
+        )
+        escaped_caption_text = html.escape(caption_text)
+        escaped_header_text = html.escape(media.header_title_text or "")
+        # For post shares, the media title starts with the username, which is also the header.
+        # That part should be bolded.
+        if (
+            escaped_header_text
+            and escaped_caption_text
+            and escaped_caption_text.startswith(escaped_header_text)
+        ):
+            escaped_caption_text = (
                 f"<strong>{escaped_header_text}</strong>"
-                f"{escaped_title_text[len(escaped_header_text):]}"
+                f"{escaped_caption_text[len(escaped_header_text):]}"
+            )
+        if item.message_item_type == "animated_media":
+            anim = await self._reupload_instagram_file(
+                source,
+                url=item.animated_media.images.fixed_height.webp,
+                msgtype=MessageType.IMAGE,
+                info=ImageInfo(
+                    width=int(item.animated_media.images.fixed_height.width),
+                    height=int(item.animated_media.images.fixed_height.height),
+                ),
+                intent=intent,
             )
+            inline_img = (
+                f'<img src="{anim.url}" width={anim.info.width} height={anim.info.height}/>'
+            )
+            escaped_caption_text = (
+                f"{escaped_caption_text}<br/>{inline_img}" if escaped_caption_text else inline_img
+            )
+        target_url_pretty = str(URL(media.target_url).with_query(None)).replace("https://www.", "")
         caption_formatted_body = (
-            f"<blockquote>{escaped_title_text}</blockquote>"
-            f'<a href="{media.target_url}">{target_url_pretty}</a>'
+            f"<blockquote>{escaped_caption_text}</blockquote>" if escaped_caption_text else ""
         )
+        caption_formatted_body += f'<p><a href="{media.target_url}">{target_url_pretty}</a></p>'
+        # Add auxiliary text as prefix for caption
+        if item.auxiliary_text:
+            caption_formatted_body = (
+                f"<p>{html.escape(item.auxiliary_text)}</p>{caption_formatted_body}"
+            )
+            caption_body = f"{item.auxiliary_text}\n\n{caption_body}"
         content.external_url = media.target_url
         caption = TextMessageEventContent(
             msgtype=MessageType.TEXT,
@@ -1067,7 +1119,7 @@ class Portal(DBPortal, BasePortal):
                 caption.ensure_has_html()
                 content.body += f"\n\n{caption.body}"
                 content.formatted_body = (
-                    f"<p><b>{content.formatted_body}</b></p><p>{caption.formatted_body}</p>"
+                    f"<p><b>{content.formatted_body}</b></p>{caption.formatted_body}"
                 )
             else:
                 content["filename"] = content.body
@@ -1086,6 +1138,7 @@ class Portal(DBPortal, BasePortal):
 
         return event_id
 
+    # TODO this is probably unused
     async def _handle_instagram_reel_share(
         self, source: u.User, intent: IntentAPI, item: ThreadItem
     ) -> EventID | None:
@@ -1394,7 +1447,16 @@ class Portal(DBPortal, BasePortal):
             intent = sender.intent_for(self)
         event_id = None
         needs_handling = True
-        if item.media or item.animated_media or item.voice_media or item.visual_media:
+        allow_text_handle = True
+        if (
+            item.xma_media_share
+            or item.xma_reel_share
+            or item.xma_reel_mention
+            or item.xma_story_share
+        ):
+            event_id = await self._handle_instagram_xma_media_share(source, intent, item)
+            allow_text_handle = False
+        elif item.media or item.animated_media or item.voice_media or item.visual_media:
             content = await self._convert_instagram_media(source, intent, item)
             event_id = await self._send_message(intent, content, timestamp=item.timestamp_ms)
         elif item.location:
@@ -1411,14 +1473,12 @@ class Portal(DBPortal, BasePortal):
             or item.felix_share
         ):
             event_id = await self._handle_instagram_media_share(source, intent, item)
-        elif item.xma_media_share:
-            event_id = await self._handle_instagram_xma_media_share(source, intent, item)
         elif item.action_log:
             # These probably don't need to be bridged
             needs_handling = False
             self.log.debug(f"Ignoring action log message {item.item_id}")
         # TODO handle item.clip?
-        if item.text:
+        if item.text and allow_text_handle:
             event_id = await self._handle_instagram_text(intent, item, item.text)
         elif item.like:
             # We handle likes as text because Matrix clients do big emoji on their own.