diff --git a/ROADMAP.md b/ROADMAP.md index d2d5b640af71a2227472ebb0d1c117493e80e920..a4bbc415c4ae543871e74dfc49270b97c73db152 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,7 +15,7 @@ * [ ] Mentions * [x] Message redactions * [x] Presence - * [ ] Typing notifications + * [x] Typing notifications * [x] Read receipts * [ ] Power level * [ ] Membership actions diff --git a/mautrix_facebook/custom_puppet.py b/mautrix_facebook/custom_puppet.py index 7a9c229a06cf2e3c10f3dd3b8dfd8f2b79fca8cd..9d86f93d606615fe68e15639650500ffced7f184 100644 --- a/mautrix_facebook/custom_puppet.py +++ b/mautrix_facebook/custom_puppet.py @@ -24,12 +24,12 @@ from aiohttp import ClientConnectionError from mautrix.types import (UserID, FilterID, Filter, RoomEventFilter, RoomFilter, EventFilter, EventType, SyncToken, RoomID, Event, PresenceState) from mautrix.appservice import AppService, IntentAPI -from mautrix.errors import IntentError, MatrixRequestError +from mautrix.errors import IntentError, MatrixError, MatrixRequestError from . import matrix as m -class CustomPuppetError(Exception): +class CustomPuppetError(MatrixError): """Base class for double puppeting setup errors.""" @@ -214,10 +214,10 @@ class CustomPuppetMixin(ABC): if next_batch is not None: self.handle_sync(sync_resp) next_batch = sync_resp.get("next_batch", None) - except (MatrixRequestError, ClientConnectionError) as e: + except (MatrixError, ClientConnectionError) as e: + errors += 1 wait = min(errors, 11) ** 2 self.log.warning(f"Syncer for {custom_mxid} errored: {e}. " f"Waiting for {wait} seconds...") - errors += 1 await asyncio.sleep(wait) self.log.debug(f"Syncer for custom puppet {custom_mxid} stopped.") diff --git a/mautrix_facebook/matrix.py b/mautrix_facebook/matrix.py index 17ba8c6c0ada656e770e9030d480a183f5a0ae00..c3e0959531800c0a797cda50cc0b60f7e1d72878 100644 --- a/mautrix_facebook/matrix.py +++ b/mautrix_facebook/matrix.py @@ -251,7 +251,13 @@ class MatrixHandler: user.setActiveStatus(evt.content.presence == PresenceState.ONLINE) async def handle_typing(self, evt: TypingEvent) -> None: - pass + portal = po.Portal.get_by_mxid(evt.room_id) + if not portal: + return + + users = (u.User.get_by_mxid(mxid, create=False) for mxid in evt.content.user_ids) + await portal.handle_matrix_typing({user for user in users + if user is not None}) @staticmethod async def handle_receipt(evt: ReceiptEvent) -> None: diff --git a/mautrix_facebook/portal.py b/mautrix_facebook/portal.py index 071b95c502c194a5366b447d6036e2c764fe9a2c..8d741a80d74772892545550e5962133dd43d7ed1 100644 --- a/mautrix_facebook/portal.py +++ b/mautrix_facebook/portal.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. -from typing import Dict, Deque, Optional, Tuple, Union, TYPE_CHECKING +from typing import Dict, Deque, Optional, Tuple, Union, Set, TYPE_CHECKING from collections import deque import asyncio import logging @@ -25,7 +25,7 @@ import magic from fbchat.models import (ThreadType, Thread, User as FBUser, Group as FBGroup, Page as FBPage, Message as FBMessage, Sticker as FBSticker, AudioAttachment, VideoAttachment, FileAttachment, ImageAttachment, LocationAttachment, - ShareAttachment) + ShareAttachment, TypingStatus) from mautrix.types import (RoomID, EventType, ContentURI, MessageEventContent, EventID, ImageInfo, MessageType, LocationMessageEventContent, LocationInfo, ThumbnailInfo, FileInfo, AudioInfo, VideoInfo, Format, @@ -80,6 +80,7 @@ class Portal: _avatar_uri: Optional[ContentURI] _send_locks: Dict[str, asyncio.Lock] _noop_lock: FakeLock = FakeLock() + _typing: Set['u.User'] def __init__(self, fbid: str, fb_receiver: str, fb_type: ThreadType, mxid: Optional[RoomID] = None, name: str = "", photo_id: str = "", @@ -100,6 +101,7 @@ class Portal: self._dedup = deque(maxlen=100) self._avatar_uri = None self._send_locks = {} + self._typing = set() self.log = self.log.getChild(self.fbid_log) @@ -376,6 +378,14 @@ class Portal: else: self.log.debug(f"{user.mxid} left portal to {self.fbid}") + async def handle_matrix_typing(self, users: Set['u.User']) -> None: + stopped_typing = [user.setTypingStatus(TypingStatus.STOPPED, self.fbid, self.fb_type) + for user in self._typing - users] + started_typing = [user.setTypingStatus(TypingStatus.TYPING, self.fbid, self.fb_type) + for user in users - self._typing] + self._typing = users + await asyncio.gather(*stopped_typing, *started_typing, loop=self.loop) + # endregion # region Facebook event handling @@ -509,7 +519,7 @@ class Portal: self._dedup.appendleft(message_id) # When we fetch thread info manually, we only get the URL instead of the ID, # so we can't use the actual ID here either. - #self.photo_id = new_photo_id + # self.photo_id = new_photo_id photo_url = await source.fetchImageUrl(new_photo_id) photo_id = self._get_photo_id(photo_url) if self.photo_id == photo_id: