diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index f67eee2e77a5..c3b65ccdcdbc 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -205,6 +205,7 @@ def __init__(self, hs: "HomeServer"): self.VALID_PRESENCE += (PresenceState.BUSY,) active_presence = self.store.take_presence_startup_info() + # The combined status across all user devices. self.user_to_current_state = {state.user_id: state for state in active_presence} @abc.abstractmethod @@ -1077,11 +1078,13 @@ async def bump_presence_active_time( if device_state.state == PresenceState.UNAVAILABLE: device_state.state = PresenceState.ONLINE + # Update the user state, this will always update last_active_ts and + # might update the presence state. prev_state = await self.current_state_for_user(user_id) - - new_fields: Dict[str, Any] = {"last_active_ts": now} - if prev_state.state == PresenceState.UNAVAILABLE: - new_fields["state"] = PresenceState.ONLINE + new_fields: Dict[str, Any] = { + "last_active_ts": now, + "state": _combine_device_states(devices.values()), + } await self._update_states([prev_state.copy_and_replace(**new_fields)]) @@ -1225,19 +1228,20 @@ async def update_external_syncs_clear(self, process_id: str) -> None: time_now_ms = self.clock.time_msec() # Mark each device as having a last sync time. + updated_users = set() for user_id, device_id in process_presence: device_state = self._user_to_device_to_current_state.setdefault( user_id, {} ).setdefault( device_id, UserDevicePresenceState.default(user_id, device_id) ) + device_state.last_sync_ts = time_now_ms + updated_users.add(user_id) # Update each user (and insert into the appropriate timers to check if # they've gone offline). - prev_states = await self.current_state_for_users( - {user_id for user_id, device_id in process_presence} - ) + prev_states = await self.current_state_for_users(updated_users) await self._update_states( [ prev_state.copy_and_replace(last_user_sync_ts=time_now_ms) @@ -1370,6 +1374,9 @@ async def set_state( if is_sync: device_state.last_sync_ts = now + # Based on the state of each user's device calculate the new presence state. + presence = _combine_device_states(devices.values()) + new_fields = {"state": presence} if not ignore_status_msg: @@ -2129,6 +2136,46 @@ def handle_update( return new_state, persist_and_notify, federation_ping +PRESENCE_BY_PRIORITY = { + PresenceState.BUSY: 4, + PresenceState.ONLINE: 3, + PresenceState.UNAVAILABLE: 2, + PresenceState.OFFLINE: 1, +} + + +def _combine_device_states( + device_states: Iterable[UserDevicePresenceState], +) -> str: + """ + Find the device to use presence information from. + + Orders devices by priority, then last_active_ts. + + Args: + device_states: An iterable of device presence states + + Return: + The combined presence state. + """ + + # Based on (all) the user's devices calculate the new presence state. + presence = PresenceState.OFFLINE + last_active_ts = -1 + + # Find the device to use the presence state of based on the presence priority, + # but tie-break with how recently the device has been seen. + for device_state in device_states: + if (PRESENCE_BY_PRIORITY[device_state.state], device_state.last_active_ts) > ( + PRESENCE_BY_PRIORITY[presence], + last_active_ts, + ): + presence = device_state.state + last_active_ts = device_state.last_active_ts + + return presence + + async def get_interested_parties( store: DataStore, presence_router: PresenceRouter, states: List[UserPresenceState] ) -> Tuple[Dict[str, List[UserPresenceState]], Dict[str, List[UserPresenceState]]]: