Browse Source

Implement Matrix->Instagram read receipts and typing notifications

Tulir Asokan 4 years ago
parent
commit
db982c705e
5 changed files with 34 additions and 10 deletions
  1. 2 2
      ROADMAP.md
  2. 1 1
      mauigpapi/types/mqtt.py
  3. 6 5
      mautrix_instagram/matrix.py
  4. 21 2
      mautrix_instagram/portal.py
  5. 4 0
      mautrix_instagram/user.py

+ 2 - 2
ROADMAP.md

@@ -12,8 +12,8 @@
   * [x] Message redactions
   * [x] Message redactions
   * [x] Message reactions
   * [x] Message reactions
   * [ ] Presence
   * [ ] Presence
-  * [ ] Typing notifications
-  * [ ] Read receipts
+  * [x] Typing notifications
+  * [x] Read receipts
 * Instagram → Matrix
 * Instagram → Matrix
   * [x] Message content
   * [x] Message content
     * [x] Text
     * [x] Text

+ 1 - 1
mauigpapi/types/mqtt.py

@@ -63,7 +63,7 @@ class CommandResponsePayload(SerializableAttrs['CommandResponsePayload']):
 class CommandResponse(SerializableAttrs['CommandResponse']):
 class CommandResponse(SerializableAttrs['CommandResponse']):
     action: str
     action: str
     status: str
     status: str
-    status_code: str
+    status_code: Optional[str] = None
     payload: CommandResponsePayload
     payload: CommandResponsePayload
 
 
 
 

+ 6 - 5
mautrix_instagram/matrix.py

@@ -101,14 +101,15 @@ class MatrixHandler(BaseMatrixHandler):
         message = await DBMessage.get_by_mxid(event_id, portal.mxid)
         message = await DBMessage.get_by_mxid(event_id, portal.mxid)
         if not message or message.is_internal:
         if not message or message.is_internal:
             return
             return
-        # TODO implement
-        # user.log.debug(f"Marking messages in {portal.thread_id} read up to {message.item_id}")
-        # await user.client.conversation(portal.thread_id).mark_read(message.item_id)
+        user.log.debug(f"Marking {message.item_id} in {portal.thread_id} as read")
+        await user.mqtt.mark_seen(portal.thread_id, message.item_id)
 
 
     @staticmethod
     @staticmethod
     async def handle_typing(room_id: RoomID, typing: List[UserID]) -> None:
     async def handle_typing(room_id: RoomID, typing: List[UserID]) -> None:
-        # TODO implement
-        pass
+        portal = await po.Portal.get_by_mxid(room_id)
+        if not portal:
+            return
+        await portal.handle_matrix_typing(set(typing))
 
 
     async def handle_event(self, evt: Event) -> None:
     async def handle_event(self, evt: Event) -> None:
         if evt.type == EventType.ROOM_REDACTION:
         if evt.type == EventType.ROOM_REDACTION:

+ 21 - 2
mautrix_instagram/portal.py

@@ -25,12 +25,13 @@ import magic
 
 
 from mauigpapi.types import (Thread, ThreadUser, ThreadItem, RegularMediaItem, MediaType,
 from mauigpapi.types import (Thread, ThreadUser, ThreadItem, RegularMediaItem, MediaType,
                              ReactionStatus, Reaction, AnimatedMediaItem, ThreadItemType,
                              ReactionStatus, Reaction, AnimatedMediaItem, ThreadItemType,
-                             VoiceMediaItem, ExpiredMediaItem, MessageSyncMessage, ReelShareType)
+                             VoiceMediaItem, ExpiredMediaItem, MessageSyncMessage, ReelShareType,
+                             TypingStatus)
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.bridge import BasePortal, NotificationDisabler
 from mautrix.bridge import BasePortal, NotificationDisabler
 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)
+                           ContentURI, EncryptedFile, LocationMessageEventContent, Format, UserID)
 from mautrix.errors import MatrixError, MForbidden
 from mautrix.errors import MatrixError, MForbidden
 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
@@ -79,6 +80,7 @@ class Portal(DBPortal, BasePortal):
     _last_participant_update: Set[int]
     _last_participant_update: Set[int]
     _reaction_lock: asyncio.Lock
     _reaction_lock: asyncio.Lock
     _backfill_leave: Optional[Set[IntentAPI]]
     _backfill_leave: Optional[Set[IntentAPI]]
+    _typing: Set[UserID]
 
 
     def __init__(self, thread_id: str, receiver: int, other_user_pk: Optional[int],
     def __init__(self, thread_id: str, receiver: int, other_user_pk: Optional[int],
                  mxid: Optional[RoomID] = None, name: Optional[str] = None, encrypted: bool = False
                  mxid: Optional[RoomID] = None, name: Optional[str] = None, encrypted: bool = False
@@ -96,6 +98,7 @@ class Portal(DBPortal, BasePortal):
         self._backfill_leave = None
         self._backfill_leave = None
         self._main_intent = None
         self._main_intent = None
         self._reaction_lock = asyncio.Lock()
         self._reaction_lock = asyncio.Lock()
+        self._typing = set()
 
 
     @property
     @property
     def is_direct(self) -> bool:
     def is_direct(self) -> bool:
@@ -254,6 +257,22 @@ class Portal(DBPortal, BasePortal):
             except Exception:
             except Exception:
                 self.log.exception("Removing message failed")
                 self.log.exception("Removing message failed")
 
 
+    async def handle_matrix_typing(self, users: Set[UserID]) -> None:
+        if users == self._typing:
+            return
+        old_typing = self._typing
+        self._typing = users
+        await self._handle_matrix_typing(old_typing - users, TypingStatus.OFF)
+        await self._handle_matrix_typing(users - old_typing, TypingStatus.TEXT)
+
+    async def _handle_matrix_typing(self, users: Set[UserID], status: TypingStatus) -> None:
+        for mxid in users:
+            user = await u.User.get_by_mxid(mxid, create=False)
+            if not user or not await user.is_logged_in() or user.remote_typing_status == status:
+                continue
+            user.remote_typing_status = None
+            await user.mqtt.indicate_activity(self.thread_id, status)
+
     async def handle_matrix_leave(self, user: 'u.User') -> None:
     async def handle_matrix_leave(self, user: 'u.User') -> None:
         if self.is_direct:
         if self.is_direct:
             self.log.info(f"{user.mxid} left private chat portal with {self.other_user_pk}")
             self.log.info(f"{user.mxid} left private chat portal with {self.other_user_pk}")

+ 4 - 0
mautrix_instagram/user.py

@@ -63,6 +63,7 @@ class User(DBUser, BaseUser):
     _notice_room_lock: asyncio.Lock
     _notice_room_lock: asyncio.Lock
     _notice_send_lock: asyncio.Lock
     _notice_send_lock: asyncio.Lock
     _is_logged_in: bool
     _is_logged_in: bool
+    remote_typing_status: Optional[TypingStatus]
 
 
     def __init__(self, mxid: UserID, igpk: Optional[int] = None,
     def __init__(self, mxid: UserID, igpk: Optional[int] = None,
                  state: Optional[AndroidState] = None, notice_room: Optional[RoomID] = None
                  state: Optional[AndroidState] = None, notice_room: Optional[RoomID] = None
@@ -81,6 +82,7 @@ class User(DBUser, BaseUser):
         self._is_logged_in = False
         self._is_logged_in = False
         self._listen_task = None
         self._listen_task = None
         self.command_status = None
         self.command_status = None
+        self.remote_typing_status = None
 
 
     @classmethod
     @classmethod
     def init_cls(cls, bridge: 'InstagramBridge') -> AsyncIterable[Awaitable[None]]:
     def init_cls(cls, bridge: 'InstagramBridge') -> AsyncIterable[Awaitable[None]]:
@@ -301,6 +303,8 @@ class User(DBUser, BaseUser):
             return
             return
 
 
         is_typing = evt.value.activity_status != TypingStatus.OFF
         is_typing = evt.value.activity_status != TypingStatus.OFF
+        if puppet.pk == self.igpk:
+            self.remote_typing_status = TypingStatus.TEXT if is_typing else TypingStatus.OFF
         await puppet.intent_for(portal).set_typing(portal.mxid, is_typing=is_typing,
         await puppet.intent_for(portal).set_typing(portal.mxid, is_typing=is_typing,
                                                    timeout=evt.value.ttl)
                                                    timeout=evt.value.ttl)