diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 4c87718337..e031cdf3b3 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -40,7 +40,9 @@ from synapse.types import ( StrCollection, StreamKeyType, UserID, + create_requester, ) +from synapse.types.state import StateFilter from synapse.util.caches.stream_change_cache import StreamChangeCache from synapse.util.metrics import Measure from synapse.util.retryutils import filter_destinations_by_retry_limiter @@ -58,6 +60,8 @@ logger = logging.getLogger(__name__) class RoomMember: room_id: str user_id: str + type: str + device_id: Optional[str] = None # How often we expect remote servers to resend us presence. @@ -108,6 +112,8 @@ class FollowerTypingHandler: self.clock.looping_call(self._handle_timeouts, 5000) self.clock.looping_call(self._prune_old_typing, FORGET_TIMEOUT) + self.state = hs.get_state_handler() + self.event_creation_handler = hs.get_event_creation_handler() def _reset(self) -> None: """Reset the typing handler's data caches.""" @@ -130,9 +136,9 @@ class FollowerTypingHandler: members = set(self.wheel_timer.fetch(now)) for member in members: - self._handle_timeout_for_member(now, member) + await self._handle_timeout_for_member(now, member) - def _handle_timeout_for_member(self, now: int, member: RoomMember) -> None: + async def _handle_timeout_for_member(self, now: int, member: RoomMember) -> None: if not self.is_typing(member): # Nothing to do if they're no longer typing return @@ -283,8 +289,8 @@ class TypingWriterHandler(FollowerTypingHandler): "TypingStreamChangeCache", self._latest_room_serial ) - def _handle_timeout_for_member(self, now: int, member: RoomMember) -> None: - super()._handle_timeout_for_member(now, member) + async def _handle_timeout_for_member(self, now: int, member: RoomMember) -> None: + await super()._handle_timeout_for_member(now, member) if not self.is_typing(member): # Nothing to do if they're no longer typing @@ -293,11 +299,16 @@ class TypingWriterHandler(FollowerTypingHandler): until = self._member_typing_until.get(member, None) if not until or until <= now: logger.info("Timing out typing for: %s", member.user_id) - self._stopped_typing(member) + await self._stopped_typing(member) return async def started_typing( - self, target_user: UserID, requester: Requester, room_id: str, timeout: int + self, + target_user: UserID, + requester: Requester, + room_id: str, + timeout: int, + type: str, ) -> None: target_user_id = target_user.to_string() @@ -316,7 +327,12 @@ class TypingWriterHandler(FollowerTypingHandler): logger.debug("%s has started typing in %s", target_user_id, room_id) - member = RoomMember(room_id=room_id, user_id=target_user_id) + member = RoomMember( + room_id=room_id, + user_id=target_user_id, + device_id=requester.device_id, + type=type, + ) was_present = member.user_id in self._room_typing.get(room_id, set()) @@ -332,7 +348,7 @@ class TypingWriterHandler(FollowerTypingHandler): self._push_update(member=member, typing=True) async def stopped_typing( - self, target_user: UserID, requester: Requester, room_id: str + self, target_user: UserID, requester: Requester, room_id: str, type: str ) -> None: target_user_id = target_user.to_string() @@ -351,17 +367,90 @@ class TypingWriterHandler(FollowerTypingHandler): logger.debug("%s has stopped typing in %s", target_user_id, room_id) - member = RoomMember(room_id=room_id, user_id=target_user_id) + member = RoomMember( + room_id=room_id, + user_id=target_user_id, + device_id=requester.device_id, + type=type, + ) - self._stopped_typing(member) + await self._stopped_typing(member) - def user_left_room(self, user: UserID, room_id: str) -> None: + async def user_left_room(self, user: UserID, room_id: str) -> None: user_id = user.to_string() if self.is_mine_id(user_id): - member = RoomMember(room_id=room_id, user_id=user_id) - self._stopped_typing(member) + # We use a device_id of "*" to indicate that all devices will have left the room + member = RoomMember(room_id=room_id, user_id=user_id, device_id="*") + await self._stopped_typing(member) + + async def _stopped_typing(self, member: RoomMember) -> None: + logger.debug( + "User %s has stopped typing (type=%s) in %s on device %s", + member.user_id, + member.type, + member.room_id, + member.device_id, + ) + + if ( + member.type == "org.matrix.msc3401.call.member" + and member.device_id is not None + ): + # Check room state to see if any MSC3401 member events needing removal + event_filter = [ + ("org.matrix.msc3401.call.member", member.user_id), + ] + + state_filter = StateFilter.from_types(event_filter) + + state_ids = await self._storage_controllers.state.get_current_state_ids( + member.room_id, + state_filter, + ) + + state_events = await self.store.get_events(state_ids.values()) + + for event in state_events.values(): + if "memberships" in event.content: + # n.b. Simplified type + memberships: List[Dict[str, str]] = event.content["memberships"] + new_memberships = [] + modified = False + if member.device_id == "*": + # user has left room so remove all memberships if the were any + modified = len(memberships) > 0 + else: + # find a membership for this device_id + for membership in memberships: + if membership["device_id"] == member.device_id: + modified = True + else: + new_memberships.append(membership) + + if modified: + # send state event with new memberships + logger.info( + "Found matching MSC3401 call member event to cleanup. New memberships is %s", + new_memberships, + ) + requester = create_requester( + member.user_id, authenticated_entity=member.user_id + ) + + await self.event_creation_handler.create_and_send_nonmember_event( + requester, + { + "type": "org.matrix.msc3401.call.member", + "state_key": member.user_id, + "room_id": member.room_id, + "sender": member.user_id, + "content": { + "memberships": new_memberships, + }, + }, + ratelimit=False, + ) - def _stopped_typing(self, member: RoomMember) -> None: if member.user_id not in self._room_typing.get(member.room_id, set()): # No point return diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py index 903c74f6d8..33d6f05f69 100644 --- a/synapse/rest/client/room.py +++ b/synapse/rest/client/room.py @@ -1256,6 +1256,8 @@ class RoomTypingRestServlet(RestServlet): # Limit timeout to stop people from setting silly typing timeouts. timeout = min(content.get("timeout", 30000), 120000) + type = content.get("io.element.type", "m.typing") + # Defer getting the typing handler since it will raise on WORKER_PATTERNS. typing_handler = self.hs.get_typing_writer_handler() @@ -1266,10 +1268,14 @@ class RoomTypingRestServlet(RestServlet): requester=requester, room_id=room_id, timeout=timeout, + type=type, ) else: await typing_handler.stopped_typing( - target_user=target_user, requester=requester, room_id=room_id + target_user=target_user, + requester=requester, + room_id=room_id, + type=type, ) except ShadowBanError: # Pretend this worked without error.