瀏覽代碼

Implement Matrix->Instagram read receipts and typing notifications

Tulir Asokan 4 年之前
父節點
當前提交
db982c705e
共有 5 個文件被更改,包括 34 次插入10 次删除
  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 reactions
   * [ ] Presence
-  * [ ] Typing notifications
-  * [ ] Read receipts
+  * [x] Typing notifications
+  * [x] Read receipts
 * Instagram → Matrix
   * [x] Message content
     * [x] Text

+ 1 - 1
mauigpapi/types/mqtt.py

@@ -63,7 +63,7 @@ class CommandResponsePayload(SerializableAttrs['CommandResponsePayload']):
 class CommandResponse(SerializableAttrs['CommandResponse']):
     action: str
     status: str
-    status_code: str
+    status_code: Optional[str] = None
     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)
         if not message or message.is_internal:
             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
     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:
         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,
                              ReactionStatus, Reaction, AnimatedMediaItem, ThreadItemType,
-                             VoiceMediaItem, ExpiredMediaItem, MessageSyncMessage, ReelShareType)
+                             VoiceMediaItem, ExpiredMediaItem, MessageSyncMessage, ReelShareType,
+                             TypingStatus)
 from mautrix.appservice import AppService, IntentAPI
 from mautrix.bridge import BasePortal, NotificationDisabler
 from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType, ImageInfo,
                            VideoInfo, MediaMessageEventContent, TextMessageEventContent, AudioInfo,
-                           ContentURI, EncryptedFile, LocationMessageEventContent, Format)
+                           ContentURI, EncryptedFile, LocationMessageEventContent, Format, UserID)
 from mautrix.errors import MatrixError, MForbidden
 from mautrix.util.simple_lock import SimpleLock
 from mautrix.util.network_retry import call_with_net_retry
@@ -79,6 +80,7 @@ class Portal(DBPortal, BasePortal):
     _last_participant_update: Set[int]
     _reaction_lock: asyncio.Lock
     _backfill_leave: Optional[Set[IntentAPI]]
+    _typing: Set[UserID]
 
     def __init__(self, thread_id: str, receiver: int, other_user_pk: Optional[int],
                  mxid: Optional[RoomID] = None, name: Optional[str] = None, encrypted: bool = False
@@ -96,6 +98,7 @@ class Portal(DBPortal, BasePortal):
         self._backfill_leave = None
         self._main_intent = None
         self._reaction_lock = asyncio.Lock()
+        self._typing = set()
 
     @property
     def is_direct(self) -> bool:
@@ -254,6 +257,22 @@ class Portal(DBPortal, BasePortal):
             except Exception:
                 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:
         if self.is_direct:
             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_send_lock: asyncio.Lock
     _is_logged_in: bool
+    remote_typing_status: Optional[TypingStatus]
 
     def __init__(self, mxid: UserID, igpk: Optional[int] = None,
                  state: Optional[AndroidState] = None, notice_room: Optional[RoomID] = None
@@ -81,6 +82,7 @@ class User(DBUser, BaseUser):
         self._is_logged_in = False
         self._listen_task = None
         self.command_status = None
+        self.remote_typing_status = None
 
     @classmethod
     def init_cls(cls, bridge: 'InstagramBridge') -> AsyncIterable[Awaitable[None]]:
@@ -301,6 +303,8 @@ class User(DBUser, BaseUser):
             return
 
         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,
                                                    timeout=evt.value.ttl)