mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-25 02:55:46 +03:00
Prevent a sync request from removing a user's busy presence status (#12213)
In trying to use the MSC3026 busy presence status, the user's status would be set back to 'online' next time they synced. This change makes it so that syncing does not affect a user's presence status if it is currently set to 'busy': it must be removed through the presence API. The MSC defers to implementations on the behaviour of busy presence, so this ought to remain compatible with the MSC.
This commit is contained in:
parent
e3a49f4784
commit
73d8ded0b0
5 changed files with 133 additions and 18 deletions
1
changelog.d/12213.bugfix
Normal file
1
changelog.d/12213.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Prevent a sync request from removing a user's busy presence status.
|
|
@ -16,7 +16,7 @@ import logging
|
||||||
import random
|
import random
|
||||||
from typing import TYPE_CHECKING, Iterable, List, Optional
|
from typing import TYPE_CHECKING, Iterable, List, Optional
|
||||||
|
|
||||||
from synapse.api.constants import EduTypes, EventTypes, Membership
|
from synapse.api.constants import EduTypes, EventTypes, Membership, PresenceState
|
||||||
from synapse.api.errors import AuthError, SynapseError
|
from synapse.api.errors import AuthError, SynapseError
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.utils import SerializeEventConfig
|
from synapse.events.utils import SerializeEventConfig
|
||||||
|
@ -67,7 +67,9 @@ class EventStreamHandler:
|
||||||
presence_handler = self.hs.get_presence_handler()
|
presence_handler = self.hs.get_presence_handler()
|
||||||
|
|
||||||
context = await presence_handler.user_syncing(
|
context = await presence_handler.user_syncing(
|
||||||
auth_user_id, affect_presence=affect_presence
|
auth_user_id,
|
||||||
|
affect_presence=affect_presence,
|
||||||
|
presence_state=PresenceState.ONLINE,
|
||||||
)
|
)
|
||||||
with context:
|
with context:
|
||||||
if timeout:
|
if timeout:
|
||||||
|
|
|
@ -151,7 +151,7 @@ class BasePresenceHandler(abc.ABC):
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def user_syncing(
|
async def user_syncing(
|
||||||
self, user_id: str, affect_presence: bool
|
self, user_id: str, affect_presence: bool, presence_state: str
|
||||||
) -> ContextManager[None]:
|
) -> ContextManager[None]:
|
||||||
"""Returns a context manager that should surround any stream requests
|
"""Returns a context manager that should surround any stream requests
|
||||||
from the user.
|
from the user.
|
||||||
|
@ -165,6 +165,7 @@ class BasePresenceHandler(abc.ABC):
|
||||||
affect_presence: If false this function will be a no-op.
|
affect_presence: If false this function will be a no-op.
|
||||||
Useful for streams that are not associated with an actual
|
Useful for streams that are not associated with an actual
|
||||||
client that is being used by a user.
|
client that is being used by a user.
|
||||||
|
presence_state: The presence state indicated in the sync request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -228,6 +229,11 @@ class BasePresenceHandler(abc.ABC):
|
||||||
|
|
||||||
return states
|
return states
|
||||||
|
|
||||||
|
async def current_state_for_user(self, user_id: str) -> UserPresenceState:
|
||||||
|
"""Get the current presence state for a user."""
|
||||||
|
res = await self.current_state_for_users([user_id])
|
||||||
|
return res[user_id]
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def set_state(
|
async def set_state(
|
||||||
self,
|
self,
|
||||||
|
@ -461,7 +467,7 @@ class WorkerPresenceHandler(BasePresenceHandler):
|
||||||
self.send_user_sync(user_id, False, last_sync_ms)
|
self.send_user_sync(user_id, False, last_sync_ms)
|
||||||
|
|
||||||
async def user_syncing(
|
async def user_syncing(
|
||||||
self, user_id: str, affect_presence: bool
|
self, user_id: str, affect_presence: bool, presence_state: str
|
||||||
) -> ContextManager[None]:
|
) -> ContextManager[None]:
|
||||||
"""Record that a user is syncing.
|
"""Record that a user is syncing.
|
||||||
|
|
||||||
|
@ -471,6 +477,17 @@ class WorkerPresenceHandler(BasePresenceHandler):
|
||||||
if not affect_presence or not self._presence_enabled:
|
if not affect_presence or not self._presence_enabled:
|
||||||
return _NullContextManager()
|
return _NullContextManager()
|
||||||
|
|
||||||
|
prev_state = await self.current_state_for_user(user_id)
|
||||||
|
if prev_state != PresenceState.BUSY:
|
||||||
|
# We set state here but pass ignore_status_msg = True as we don't want to
|
||||||
|
# cause the status message to be cleared.
|
||||||
|
# Note that this causes last_active_ts to be incremented which is not
|
||||||
|
# what the spec wants: see comment in the BasePresenceHandler version
|
||||||
|
# of this function.
|
||||||
|
await self.set_state(
|
||||||
|
UserID.from_string(user_id), {"presence": presence_state}, True
|
||||||
|
)
|
||||||
|
|
||||||
curr_sync = self._user_to_num_current_syncs.get(user_id, 0)
|
curr_sync = self._user_to_num_current_syncs.get(user_id, 0)
|
||||||
self._user_to_num_current_syncs[user_id] = curr_sync + 1
|
self._user_to_num_current_syncs[user_id] = curr_sync + 1
|
||||||
|
|
||||||
|
@ -942,7 +959,10 @@ class PresenceHandler(BasePresenceHandler):
|
||||||
await self._update_states([prev_state.copy_and_replace(**new_fields)])
|
await self._update_states([prev_state.copy_and_replace(**new_fields)])
|
||||||
|
|
||||||
async def user_syncing(
|
async def user_syncing(
|
||||||
self, user_id: str, affect_presence: bool = True
|
self,
|
||||||
|
user_id: str,
|
||||||
|
affect_presence: bool = True,
|
||||||
|
presence_state: str = PresenceState.ONLINE,
|
||||||
) -> ContextManager[None]:
|
) -> ContextManager[None]:
|
||||||
"""Returns a context manager that should surround any stream requests
|
"""Returns a context manager that should surround any stream requests
|
||||||
from the user.
|
from the user.
|
||||||
|
@ -956,6 +976,7 @@ class PresenceHandler(BasePresenceHandler):
|
||||||
affect_presence: If false this function will be a no-op.
|
affect_presence: If false this function will be a no-op.
|
||||||
Useful for streams that are not associated with an actual
|
Useful for streams that are not associated with an actual
|
||||||
client that is being used by a user.
|
client that is being used by a user.
|
||||||
|
presence_state: The presence state indicated in the sync request
|
||||||
"""
|
"""
|
||||||
# Override if it should affect the user's presence, if presence is
|
# Override if it should affect the user's presence, if presence is
|
||||||
# disabled.
|
# disabled.
|
||||||
|
@ -967,9 +988,25 @@ class PresenceHandler(BasePresenceHandler):
|
||||||
self.user_to_num_current_syncs[user_id] = curr_sync + 1
|
self.user_to_num_current_syncs[user_id] = curr_sync + 1
|
||||||
|
|
||||||
prev_state = await self.current_state_for_user(user_id)
|
prev_state = await self.current_state_for_user(user_id)
|
||||||
|
|
||||||
|
# If they're busy then they don't stop being busy just by syncing,
|
||||||
|
# so just update the last sync time.
|
||||||
|
if prev_state.state != PresenceState.BUSY:
|
||||||
|
# XXX: We set_state separately here and just update the last_active_ts above
|
||||||
|
# This keeps the logic as similar as possible between the worker and single
|
||||||
|
# process modes. Using set_state will actually cause last_active_ts to be
|
||||||
|
# updated always, which is not what the spec calls for, but synapse has done
|
||||||
|
# this for... forever, I think.
|
||||||
|
await self.set_state(
|
||||||
|
UserID.from_string(user_id), {"presence": presence_state}, True
|
||||||
|
)
|
||||||
|
# Retrieve the new state for the logic below. This should come from the
|
||||||
|
# in-memory cache.
|
||||||
|
prev_state = await self.current_state_for_user(user_id)
|
||||||
|
|
||||||
|
# To keep the single process behaviour consistent with worker mode, run the
|
||||||
|
# same logic as `update_external_syncs_row`, even though it looks weird.
|
||||||
if prev_state.state == PresenceState.OFFLINE:
|
if prev_state.state == PresenceState.OFFLINE:
|
||||||
# If they're currently offline then bring them online, otherwise
|
|
||||||
# just update the last sync times.
|
|
||||||
await self._update_states(
|
await self._update_states(
|
||||||
[
|
[
|
||||||
prev_state.copy_and_replace(
|
prev_state.copy_and_replace(
|
||||||
|
@ -979,6 +1016,10 @@ class PresenceHandler(BasePresenceHandler):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
# otherwise, set the new presence state & update the last sync time,
|
||||||
|
# but don't update last_active_ts as this isn't an indication that
|
||||||
|
# they've been active (even though it's probably been updated by
|
||||||
|
# set_state above)
|
||||||
else:
|
else:
|
||||||
await self._update_states(
|
await self._update_states(
|
||||||
[
|
[
|
||||||
|
@ -1086,11 +1127,6 @@ class PresenceHandler(BasePresenceHandler):
|
||||||
)
|
)
|
||||||
self.external_process_last_updated_ms.pop(process_id, None)
|
self.external_process_last_updated_ms.pop(process_id, None)
|
||||||
|
|
||||||
async def current_state_for_user(self, user_id: str) -> UserPresenceState:
|
|
||||||
"""Get the current presence state for a user."""
|
|
||||||
res = await self.current_state_for_users([user_id])
|
|
||||||
return res[user_id]
|
|
||||||
|
|
||||||
async def _persist_and_notify(self, states: List[UserPresenceState]) -> None:
|
async def _persist_and_notify(self, states: List[UserPresenceState]) -> None:
|
||||||
"""Persist states in the database, poke the notifier and send to
|
"""Persist states in the database, poke the notifier and send to
|
||||||
interested remote servers
|
interested remote servers
|
||||||
|
|
|
@ -180,13 +180,10 @@ class SyncRestServlet(RestServlet):
|
||||||
|
|
||||||
affect_presence = set_presence != PresenceState.OFFLINE
|
affect_presence = set_presence != PresenceState.OFFLINE
|
||||||
|
|
||||||
if affect_presence:
|
|
||||||
await self.presence_handler.set_state(
|
|
||||||
user, {"presence": set_presence}, True
|
|
||||||
)
|
|
||||||
|
|
||||||
context = await self.presence_handler.user_syncing(
|
context = await self.presence_handler.user_syncing(
|
||||||
user.to_string(), affect_presence=affect_presence
|
user.to_string(),
|
||||||
|
affect_presence=affect_presence,
|
||||||
|
presence_state=set_presence,
|
||||||
)
|
)
|
||||||
with context:
|
with context:
|
||||||
sync_result = await self.sync_handler.wait_for_sync_for_user(
|
sync_result = await self.sync_handler.wait_for_sync_for_user(
|
||||||
|
|
|
@ -657,6 +657,85 @@ class PresenceHandlerTestCase(unittest.HomeserverTestCase):
|
||||||
# Mark user as online and `status_msg = None`
|
# Mark user as online and `status_msg = None`
|
||||||
self._set_presencestate_with_status_msg(user_id, PresenceState.ONLINE, None)
|
self._set_presencestate_with_status_msg(user_id, PresenceState.ONLINE, None)
|
||||||
|
|
||||||
|
def test_set_presence_from_syncing_not_set(self):
|
||||||
|
"""Test that presence is not set by syncing if affect_presence is false"""
|
||||||
|
user_id = "@test:server"
|
||||||
|
status_msg = "I'm here!"
|
||||||
|
|
||||||
|
self._set_presencestate_with_status_msg(
|
||||||
|
user_id, PresenceState.UNAVAILABLE, status_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_success(
|
||||||
|
self.presence_handler.user_syncing(user_id, False, PresenceState.ONLINE)
|
||||||
|
)
|
||||||
|
|
||||||
|
state = self.get_success(
|
||||||
|
self.presence_handler.get_state(UserID.from_string(user_id))
|
||||||
|
)
|
||||||
|
# we should still be unavailable
|
||||||
|
self.assertEqual(state.state, PresenceState.UNAVAILABLE)
|
||||||
|
# and status message should still be the same
|
||||||
|
self.assertEqual(state.status_msg, status_msg)
|
||||||
|
|
||||||
|
def test_set_presence_from_syncing_is_set(self):
|
||||||
|
"""Test that presence is set by syncing if affect_presence is true"""
|
||||||
|
user_id = "@test:server"
|
||||||
|
status_msg = "I'm here!"
|
||||||
|
|
||||||
|
self._set_presencestate_with_status_msg(
|
||||||
|
user_id, PresenceState.UNAVAILABLE, status_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_success(
|
||||||
|
self.presence_handler.user_syncing(user_id, True, PresenceState.ONLINE)
|
||||||
|
)
|
||||||
|
|
||||||
|
state = self.get_success(
|
||||||
|
self.presence_handler.get_state(UserID.from_string(user_id))
|
||||||
|
)
|
||||||
|
# we should now be online
|
||||||
|
self.assertEqual(state.state, PresenceState.ONLINE)
|
||||||
|
|
||||||
|
def test_set_presence_from_syncing_keeps_status(self):
|
||||||
|
"""Test that presence set by syncing retains status message"""
|
||||||
|
user_id = "@test:server"
|
||||||
|
status_msg = "I'm here!"
|
||||||
|
|
||||||
|
self._set_presencestate_with_status_msg(
|
||||||
|
user_id, PresenceState.UNAVAILABLE, status_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
self.get_success(
|
||||||
|
self.presence_handler.user_syncing(user_id, True, PresenceState.ONLINE)
|
||||||
|
)
|
||||||
|
|
||||||
|
state = self.get_success(
|
||||||
|
self.presence_handler.get_state(UserID.from_string(user_id))
|
||||||
|
)
|
||||||
|
# our status message should be the same as it was before
|
||||||
|
self.assertEqual(state.status_msg, status_msg)
|
||||||
|
|
||||||
|
def test_set_presence_from_syncing_keeps_busy(self):
|
||||||
|
"""Test that presence set by syncing doesn't affect busy status"""
|
||||||
|
# while this isn't the default
|
||||||
|
self.presence_handler._busy_presence_enabled = True
|
||||||
|
|
||||||
|
user_id = "@test:server"
|
||||||
|
status_msg = "I'm busy!"
|
||||||
|
|
||||||
|
self._set_presencestate_with_status_msg(user_id, PresenceState.BUSY, status_msg)
|
||||||
|
|
||||||
|
self.get_success(
|
||||||
|
self.presence_handler.user_syncing(user_id, True, PresenceState.ONLINE)
|
||||||
|
)
|
||||||
|
|
||||||
|
state = self.get_success(
|
||||||
|
self.presence_handler.get_state(UserID.from_string(user_id))
|
||||||
|
)
|
||||||
|
# we should still be busy
|
||||||
|
self.assertEqual(state.state, PresenceState.BUSY)
|
||||||
|
|
||||||
def _set_presencestate_with_status_msg(
|
def _set_presencestate_with_status_msg(
|
||||||
self, user_id: str, state: str, status_msg: Optional[str]
|
self, user_id: str, state: str, status_msg: Optional[str]
|
||||||
):
|
):
|
||||||
|
|
Loading…
Reference in a new issue