Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 73d8ded

Browse files
authored
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.
1 parent e3a49f4 commit 73d8ded

File tree

5 files changed

+133
-18
lines changed

5 files changed

+133
-18
lines changed

changelog.d/12213.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prevent a sync request from removing a user's busy presence status.

synapse/handlers/events.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import random
1717
from typing import TYPE_CHECKING, Iterable, List, Optional
1818

19-
from synapse.api.constants import EduTypes, EventTypes, Membership
19+
from synapse.api.constants import EduTypes, EventTypes, Membership, PresenceState
2020
from synapse.api.errors import AuthError, SynapseError
2121
from synapse.events import EventBase
2222
from synapse.events.utils import SerializeEventConfig
@@ -67,7 +67,9 @@ async def get_stream(
6767
presence_handler = self.hs.get_presence_handler()
6868

6969
context = await presence_handler.user_syncing(
70-
auth_user_id, affect_presence=affect_presence
70+
auth_user_id,
71+
affect_presence=affect_presence,
72+
presence_state=PresenceState.ONLINE,
7173
)
7274
with context:
7375
if timeout:

synapse/handlers/presence.py

+46-10
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def __init__(self, hs: "HomeServer"):
151151

152152
@abc.abstractmethod
153153
async def user_syncing(
154-
self, user_id: str, affect_presence: bool
154+
self, user_id: str, affect_presence: bool, presence_state: str
155155
) -> ContextManager[None]:
156156
"""Returns a context manager that should surround any stream requests
157157
from the user.
@@ -165,6 +165,7 @@ async def user_syncing(
165165
affect_presence: If false this function will be a no-op.
166166
Useful for streams that are not associated with an actual
167167
client that is being used by a user.
168+
presence_state: The presence state indicated in the sync request
168169
"""
169170

170171
@abc.abstractmethod
@@ -228,6 +229,11 @@ async def current_state_for_users(
228229

229230
return states
230231

232+
async def current_state_for_user(self, user_id: str) -> UserPresenceState:
233+
"""Get the current presence state for a user."""
234+
res = await self.current_state_for_users([user_id])
235+
return res[user_id]
236+
231237
@abc.abstractmethod
232238
async def set_state(
233239
self,
@@ -461,7 +467,7 @@ def send_stop_syncing(self) -> None:
461467
self.send_user_sync(user_id, False, last_sync_ms)
462468

463469
async def user_syncing(
464-
self, user_id: str, affect_presence: bool
470+
self, user_id: str, affect_presence: bool, presence_state: str
465471
) -> ContextManager[None]:
466472
"""Record that a user is syncing.
467473
@@ -471,6 +477,17 @@ async def user_syncing(
471477
if not affect_presence or not self._presence_enabled:
472478
return _NullContextManager()
473479

480+
prev_state = await self.current_state_for_user(user_id)
481+
if prev_state != PresenceState.BUSY:
482+
# We set state here but pass ignore_status_msg = True as we don't want to
483+
# cause the status message to be cleared.
484+
# Note that this causes last_active_ts to be incremented which is not
485+
# what the spec wants: see comment in the BasePresenceHandler version
486+
# of this function.
487+
await self.set_state(
488+
UserID.from_string(user_id), {"presence": presence_state}, True
489+
)
490+
474491
curr_sync = self._user_to_num_current_syncs.get(user_id, 0)
475492
self._user_to_num_current_syncs[user_id] = curr_sync + 1
476493

@@ -942,7 +959,10 @@ async def bump_presence_active_time(self, user: UserID) -> None:
942959
await self._update_states([prev_state.copy_and_replace(**new_fields)])
943960

944961
async def user_syncing(
945-
self, user_id: str, affect_presence: bool = True
962+
self,
963+
user_id: str,
964+
affect_presence: bool = True,
965+
presence_state: str = PresenceState.ONLINE,
946966
) -> ContextManager[None]:
947967
"""Returns a context manager that should surround any stream requests
948968
from the user.
@@ -956,6 +976,7 @@ async def user_syncing(
956976
affect_presence: If false this function will be a no-op.
957977
Useful for streams that are not associated with an actual
958978
client that is being used by a user.
979+
presence_state: The presence state indicated in the sync request
959980
"""
960981
# Override if it should affect the user's presence, if presence is
961982
# disabled.
@@ -967,9 +988,25 @@ async def user_syncing(
967988
self.user_to_num_current_syncs[user_id] = curr_sync + 1
968989

969990
prev_state = await self.current_state_for_user(user_id)
991+
992+
# If they're busy then they don't stop being busy just by syncing,
993+
# so just update the last sync time.
994+
if prev_state.state != PresenceState.BUSY:
995+
# XXX: We set_state separately here and just update the last_active_ts above
996+
# This keeps the logic as similar as possible between the worker and single
997+
# process modes. Using set_state will actually cause last_active_ts to be
998+
# updated always, which is not what the spec calls for, but synapse has done
999+
# this for... forever, I think.
1000+
await self.set_state(
1001+
UserID.from_string(user_id), {"presence": presence_state}, True
1002+
)
1003+
# Retrieve the new state for the logic below. This should come from the
1004+
# in-memory cache.
1005+
prev_state = await self.current_state_for_user(user_id)
1006+
1007+
# To keep the single process behaviour consistent with worker mode, run the
1008+
# same logic as `update_external_syncs_row`, even though it looks weird.
9701009
if prev_state.state == PresenceState.OFFLINE:
971-
# If they're currently offline then bring them online, otherwise
972-
# just update the last sync times.
9731010
await self._update_states(
9741011
[
9751012
prev_state.copy_and_replace(
@@ -979,6 +1016,10 @@ async def user_syncing(
9791016
)
9801017
]
9811018
)
1019+
# otherwise, set the new presence state & update the last sync time,
1020+
# but don't update last_active_ts as this isn't an indication that
1021+
# they've been active (even though it's probably been updated by
1022+
# set_state above)
9821023
else:
9831024
await self._update_states(
9841025
[
@@ -1086,11 +1127,6 @@ async def update_external_syncs_clear(self, process_id: str) -> None:
10861127
)
10871128
self.external_process_last_updated_ms.pop(process_id, None)
10881129

1089-
async def current_state_for_user(self, user_id: str) -> UserPresenceState:
1090-
"""Get the current presence state for a user."""
1091-
res = await self.current_state_for_users([user_id])
1092-
return res[user_id]
1093-
10941130
async def _persist_and_notify(self, states: List[UserPresenceState]) -> None:
10951131
"""Persist states in the database, poke the notifier and send to
10961132
interested remote servers

synapse/rest/client/sync.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,10 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
180180

181181
affect_presence = set_presence != PresenceState.OFFLINE
182182

183-
if affect_presence:
184-
await self.presence_handler.set_state(
185-
user, {"presence": set_presence}, True
186-
)
187-
188183
context = await self.presence_handler.user_syncing(
189-
user.to_string(), affect_presence=affect_presence
184+
user.to_string(),
185+
affect_presence=affect_presence,
186+
presence_state=set_presence,
190187
)
191188
with context:
192189
sync_result = await self.sync_handler.wait_for_sync_for_user(

tests/handlers/test_presence.py

+79
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,85 @@ def test_set_presence_with_status_msg_none(self):
657657
# Mark user as online and `status_msg = None`
658658
self._set_presencestate_with_status_msg(user_id, PresenceState.ONLINE, None)
659659

660+
def test_set_presence_from_syncing_not_set(self):
661+
"""Test that presence is not set by syncing if affect_presence is false"""
662+
user_id = "@test:server"
663+
status_msg = "I'm here!"
664+
665+
self._set_presencestate_with_status_msg(
666+
user_id, PresenceState.UNAVAILABLE, status_msg
667+
)
668+
669+
self.get_success(
670+
self.presence_handler.user_syncing(user_id, False, PresenceState.ONLINE)
671+
)
672+
673+
state = self.get_success(
674+
self.presence_handler.get_state(UserID.from_string(user_id))
675+
)
676+
# we should still be unavailable
677+
self.assertEqual(state.state, PresenceState.UNAVAILABLE)
678+
# and status message should still be the same
679+
self.assertEqual(state.status_msg, status_msg)
680+
681+
def test_set_presence_from_syncing_is_set(self):
682+
"""Test that presence is set by syncing if affect_presence is true"""
683+
user_id = "@test:server"
684+
status_msg = "I'm here!"
685+
686+
self._set_presencestate_with_status_msg(
687+
user_id, PresenceState.UNAVAILABLE, status_msg
688+
)
689+
690+
self.get_success(
691+
self.presence_handler.user_syncing(user_id, True, PresenceState.ONLINE)
692+
)
693+
694+
state = self.get_success(
695+
self.presence_handler.get_state(UserID.from_string(user_id))
696+
)
697+
# we should now be online
698+
self.assertEqual(state.state, PresenceState.ONLINE)
699+
700+
def test_set_presence_from_syncing_keeps_status(self):
701+
"""Test that presence set by syncing retains status message"""
702+
user_id = "@test:server"
703+
status_msg = "I'm here!"
704+
705+
self._set_presencestate_with_status_msg(
706+
user_id, PresenceState.UNAVAILABLE, status_msg
707+
)
708+
709+
self.get_success(
710+
self.presence_handler.user_syncing(user_id, True, PresenceState.ONLINE)
711+
)
712+
713+
state = self.get_success(
714+
self.presence_handler.get_state(UserID.from_string(user_id))
715+
)
716+
# our status message should be the same as it was before
717+
self.assertEqual(state.status_msg, status_msg)
718+
719+
def test_set_presence_from_syncing_keeps_busy(self):
720+
"""Test that presence set by syncing doesn't affect busy status"""
721+
# while this isn't the default
722+
self.presence_handler._busy_presence_enabled = True
723+
724+
user_id = "@test:server"
725+
status_msg = "I'm busy!"
726+
727+
self._set_presencestate_with_status_msg(user_id, PresenceState.BUSY, status_msg)
728+
729+
self.get_success(
730+
self.presence_handler.user_syncing(user_id, True, PresenceState.ONLINE)
731+
)
732+
733+
state = self.get_success(
734+
self.presence_handler.get_state(UserID.from_string(user_id))
735+
)
736+
# we should still be busy
737+
self.assertEqual(state.state, PresenceState.BUSY)
738+
660739
def _set_presencestate_with_status_msg(
661740
self, user_id: str, state: str, status_msg: Optional[str]
662741
):

0 commit comments

Comments
 (0)