Browse Source

Implement Instagram->Matrix message replies

Tulir Asokan 4 years ago
parent
commit
dfab3e7b97
4 changed files with 53 additions and 9 deletions
  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
       * [ ] Voice messages
       * [ ] Locations
       * [ ] Locations
       * [ ] †Files
       * [ ] †Files
+    * [ ] Replies
   * [x] Message redactions
   * [x] Message redactions
   * [x] Message reactions
   * [x] Message reactions
   * [ ] Presence
   * [ ] Presence
@@ -24,6 +25,7 @@
       * [x] Voice messages
       * [x] Voice messages
       * [x] Locations
       * [x] Locations
       * [x] Story/reel share
       * [x] Story/reel share
+    * [x] Replies
   * [x] Message unsend
   * [x] Message unsend
   * [x] Message reactions
   * [x] Message reactions
   * [x] Message history
   * [x] Message history

+ 8 - 0
mauigpapi/types/thread_item.py

@@ -438,6 +438,8 @@ class ThreadItem(SerializableAttrs['ThreadItem']):
     show_forward_attribution: Optional[bool] = None
     show_forward_attribution: Optional[bool] = None
     action_log: Optional[ThreadItemActionLog] = None
     action_log: Optional[ThreadItemActionLog] = None
 
 
+    replied_to_message: Optional['ThreadItem'] = None
+
     media: Optional[RegularMediaItem] = None
     media: Optional[RegularMediaItem] = None
     voice_media: Optional[VoiceMediaItem] = None
     voice_media: Optional[VoiceMediaItem] = None
     animated_media: Optional[AnimatedMediaItem] = None
     animated_media: Optional[AnimatedMediaItem] = None
@@ -459,3 +461,9 @@ class ThreadItem(SerializableAttrs['ThreadItem']):
         except SerializerError:
         except SerializerError:
             log.debug("Failed to deserialize ThreadItem %s", data)
             log.debug("Failed to deserialize ThreadItem %s", data)
             return Obj(**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,
 from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType, ImageInfo,
                            VideoInfo, MediaMessageEventContent, TextMessageEventContent, AudioInfo,
                            VideoInfo, MediaMessageEventContent, TextMessageEventContent, AudioInfo,
                            ContentURI, EncryptedFile, LocationMessageEventContent, Format, UserID)
                            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.simple_lock import SimpleLock
 from mautrix.util.network_retry import call_with_net_retry
 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,
         content = MediaMessageEventContent(body=reuploaded.file_name, external_url=reuploaded.url,
                                            url=reuploaded.mxc, file=reuploaded.decryption_info,
                                            url=reuploaded.mxc, file=reuploaded.decryption_info,
                                            info=reuploaded.info, msgtype=reuploaded.msgtype)
                                            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)
         return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
 
 
     async def _handle_instagram_media_share(self, source: 'u.User', intent: IntentAPI,
     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()
                                 receiver=self.receiver, sender=media.user.pk).insert()
         return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
         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:
                                      ) -> EventID:
         content = TextMessageEventContent(msgtype=MessageType.TEXT, body=text)
         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:
     async def _handle_instagram_location(self, intent: IntentAPI, item: ThreadItem) -> EventID:
         loc = item.location
         loc = item.location
@@ -506,6 +508,8 @@ class Portal(DBPortal, BasePortal):
         content["format"] = str(Format.HTML)
         content["format"] = str(Format.HTML)
         content["formatted_body"] = f"Location: <a href='{url}'>{body}</a>"
         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)
         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,
     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.exception("Fatal error handling Instagram item")
             self.log.trace("Item content: %s", item.serialize())
             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,
     async def _handle_instagram_item(self, source: 'u.User', sender: 'p.Puppet', item: ThreadItem,
                                      is_backfill: bool = False) -> None:
                                      is_backfill: bool = False) -> None:
         if not isinstance(item, ThreadItem):
         if not isinstance(item, ThreadItem):
@@ -551,13 +586,12 @@ class Portal(DBPortal, BasePortal):
             elif item.media_share or item.story_share:
             elif item.media_share or item.story_share:
                 event_id = await self._handle_instagram_media_share(source, intent, item)
                 event_id = await self._handle_instagram_media_share(source, intent, item)
             if item.text:
             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:
             elif item.like:
                 # We handle likes as text because Matrix clients do big emoji on their own.
                 # 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:
             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:
             if event_id:
                 msg = DBMessage(mxid=event_id, mx_room=self.mxid, item_id=item.item_id,
                 msg = DBMessage(mxid=event_id, mx_room=self.mxid, item_id=item.item_id,
                                 receiver=self.receiver, sender=sender.pk)
                                 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
 commonmark>=0.8,<0.10
 aiohttp>=3,<4
 aiohttp>=3,<4
 yarl>=1,<2
 yarl>=1,<2
-attrs>=19.1
+attrs>=20.1
 mautrix>=0.8.14,<0.9
 mautrix>=0.8.14,<0.9
-asyncpg>=0.20,<0.22
+asyncpg>=0.20,<0.23
 pycryptodome>=3,<4
 pycryptodome>=3,<4
 paho-mqtt>=1.5,<2
 paho-mqtt>=1.5,<2