Răsfoiți Sursa

Implement Instagram->Matrix message replies

Tulir Asokan 4 ani în urmă
părinte
comite
dfab3e7b97
4 a modificat fișierele cu 53 adăugiri și 9 ștergeri
  1. 2 0
      ROADMAP.md
  2. 8 0
      mauigpapi/types/thread_item.py
  3. 41 7
      mautrix_instagram/portal.py
  4. 2 2
      requirements.txt

+ 2 - 0
ROADMAP.md

@@ -9,6 +9,7 @@
       * [ ] Voice messages
       * [ ] Locations
       * [ ] †Files
+    * [ ] Replies
   * [x] Message redactions
   * [x] Message reactions
   * [ ] Presence
@@ -24,6 +25,7 @@
       * [x] Voice messages
       * [x] Locations
       * [x] Story/reel share
+    * [x] Replies
   * [x] Message unsend
   * [x] Message reactions
   * [x] Message history

+ 8 - 0
mauigpapi/types/thread_item.py

@@ -438,6 +438,8 @@ class ThreadItem(SerializableAttrs['ThreadItem']):
     show_forward_attribution: Optional[bool] = None
     action_log: Optional[ThreadItemActionLog] = None
 
+    replied_to_message: Optional['ThreadItem'] = None
+
     media: Optional[RegularMediaItem] = None
     voice_media: Optional[VoiceMediaItem] = None
     animated_media: Optional[AnimatedMediaItem] = None
@@ -459,3 +461,9 @@ class ThreadItem(SerializableAttrs['ThreadItem']):
         except SerializerError:
             log.debug("Failed to deserialize ThreadItem %s", data)
             return Obj(**data)
+
+
+# This resolves the 'ThreadItem' string into an actual type.
+# Starting Python 3.10, all type annotations will be strings and have to be resolved like this.
+# TODO do this automatically for all SerializableAttrs somewhere in mautrix-python
+attr.resolve_types(ThreadItem)

+ 41 - 7
mautrix_instagram/portal.py

@@ -32,7 +32,7 @@ from mautrix.bridge import BasePortal, NotificationDisabler, async_getter_lock
 from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType, ImageInfo,
                            VideoInfo, MediaMessageEventContent, TextMessageEventContent, AudioInfo,
                            ContentURI, EncryptedFile, LocationMessageEventContent, Format, UserID)
-from mautrix.errors import MatrixError, MForbidden
+from mautrix.errors import MatrixError, MForbidden, MNotFound, SessionNotFound
 from mautrix.util.simple_lock import SimpleLock
 from mautrix.util.network_retry import call_with_net_retry
 
@@ -416,6 +416,7 @@ class Portal(DBPortal, BasePortal):
         content = MediaMessageEventContent(body=reuploaded.file_name, external_url=reuploaded.url,
                                            url=reuploaded.mxc, file=reuploaded.decryption_info,
                                            info=reuploaded.info, msgtype=reuploaded.msgtype)
+        await self._add_instagram_reply(content, item.replied_to_message)
         return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
 
     async def _handle_instagram_media_share(self, source: 'u.User', intent: IntentAPI,
@@ -482,10 +483,11 @@ class Portal(DBPortal, BasePortal):
                                 receiver=self.receiver, sender=media.user.pk).insert()
         return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
 
-    async def _handle_instagram_text(self, intent: IntentAPI, text: str, timestamp: int
+    async def _handle_instagram_text(self, intent: IntentAPI, item: ThreadItem, text: str,
                                      ) -> EventID:
         content = TextMessageEventContent(msgtype=MessageType.TEXT, body=text)
-        return await self._send_message(intent, content, timestamp=timestamp // 1000)
+        await self._add_instagram_reply(content, item.replied_to_message)
+        return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
 
     async def _handle_instagram_location(self, intent: IntentAPI, item: ThreadItem) -> EventID:
         loc = item.location
@@ -506,6 +508,8 @@ class Portal(DBPortal, BasePortal):
         content["format"] = str(Format.HTML)
         content["formatted_body"] = f"Location: <a href='{url}'>{body}</a>"
 
+        await self._add_instagram_reply(content, item.replied_to_message)
+
         return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
 
     async def handle_instagram_item(self, source: 'u.User', sender: 'p.Puppet', item: ThreadItem,
@@ -516,6 +520,37 @@ class Portal(DBPortal, BasePortal):
             self.log.exception("Fatal error handling Instagram item")
             self.log.trace("Item content: %s", item.serialize())
 
+    async def _add_instagram_reply(self, content: MessageEventContent,
+                                   reply_to: Optional[ThreadItem]) -> None:
+        if not reply_to:
+            return
+
+        message = await DBMessage.get_by_item_id(reply_to.item_id, self.receiver)
+        if not message:
+            return
+
+        content.set_reply(message.mxid)
+        if not isinstance(content, TextMessageEventContent):
+            return
+
+        try:
+            evt = await self.main_intent.get_event(message.mx_room, message.mxid)
+        except (MNotFound, MForbidden):
+            evt = None
+        if not evt:
+            return
+
+        if evt.type == EventType.ROOM_ENCRYPTED:
+            try:
+                evt = await self.matrix.e2ee.decrypt(evt, wait_session_timeout=0)
+            except SessionNotFound:
+                return
+
+        if isinstance(evt.content, TextMessageEventContent):
+            evt.content.trim_reply_fallback()
+
+        content.set_reply(evt)
+
     async def _handle_instagram_item(self, source: 'u.User', sender: 'p.Puppet', item: ThreadItem,
                                      is_backfill: bool = False) -> None:
         if not isinstance(item, ThreadItem):
@@ -551,13 +586,12 @@ class Portal(DBPortal, BasePortal):
             elif item.media_share or item.story_share:
                 event_id = await self._handle_instagram_media_share(source, intent, item)
             if item.text:
-                event_id = await self._handle_instagram_text(intent, item.text, item.timestamp)
+                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.
-                event_id = await self._handle_instagram_text(intent, item.like, item.timestamp)
+                event_id = await self._handle_instagram_text(intent, item, item.like)
             elif item.link:
-                event_id = await self._handle_instagram_text(intent, item.link.text,
-                                                             item.timestamp)
+                event_id = await self._handle_instagram_text(intent, item, item.link.text)
             if event_id:
                 msg = DBMessage(mxid=event_id, mx_room=self.mxid, item_id=item.item_id,
                                 receiver=self.receiver, sender=sender.pk)

+ 2 - 2
requirements.txt

@@ -3,8 +3,8 @@ python-magic>=0.4,<0.5
 commonmark>=0.8,<0.10
 aiohttp>=3,<4
 yarl>=1,<2
-attrs>=19.1
+attrs>=20.1
 mautrix>=0.8.14,<0.9
-asyncpg>=0.20,<0.22
+asyncpg>=0.20,<0.23
 pycryptodome>=3,<4
 paho-mqtt>=1.5,<2