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

Prevent a sync request from removing a user's busy presence status #12213

Merged
merged 21 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/12213.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prevent a sync request from removing a user's busy presence status.
6 changes: 4 additions & 2 deletions synapse/handlers/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import random
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.events import EventBase
from synapse.events.utils import SerializeEventConfig
Expand Down Expand Up @@ -67,7 +67,9 @@ async def get_stream(
presence_handler = self.hs.get_presence_handler()

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:
if timeout:
Expand Down
51 changes: 31 additions & 20 deletions synapse/handlers/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def __init__(self, hs: "HomeServer"):

@abc.abstractmethod
async def user_syncing(
self, user_id: str, affect_presence: bool
self, user_id: str, affect_presence: bool, presence_state: str
squahtx marked this conversation as resolved.
Show resolved Hide resolved
) -> ContextManager[None]:
"""Returns a context manager that should surround any stream requests
from the user.
Expand All @@ -165,6 +165,7 @@ async def user_syncing(
affect_presence: If false this function will be a no-op.
Useful for streams that are not associated with an actual
client that is being used by a user.
presence_state: The presence state indicated in the sync request
"""

@abc.abstractmethod
Expand Down Expand Up @@ -228,12 +229,16 @@ async def current_state_for_users(

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
async def set_state(
self,
target_user: UserID,
state: JsonDict,
ignore_status_msg: bool = False,
dbkr marked this conversation as resolved.
Show resolved Hide resolved
force_notify: bool = False,
) -> None:
"""Set the presence state of the user.
Expand Down Expand Up @@ -461,7 +466,7 @@ def send_stop_syncing(self) -> None:
self.send_user_sync(user_id, False, last_sync_ms)

async def user_syncing(
self, user_id: str, affect_presence: bool
self, user_id: str, affect_presence: bool, presence_state: str
) -> ContextManager[None]:
"""Record that a user is syncing.

Expand All @@ -471,6 +476,16 @@ async def user_syncing(
if not affect_presence or not self._presence_enabled:
return _NullContextManager()

if affect_presence:
dbkr marked this conversation as resolved.
Show resolved Hide resolved
prev_state = await self.current_state_for_user(user_id)
if prev_state != PresenceState.BUSY:
# XXX: Why does this pass force_notify = True? This is copied
# from when the sync rest handler set the presence itself, but
# I don't really understand why this would be necessary.
squahtx marked this conversation as resolved.
Show resolved Hide resolved
self.set_state(
UserID.from_string(user_id), {"presence": presence_state}, True
)

curr_sync = self._user_to_num_current_syncs.get(user_id, 0)
self._user_to_num_current_syncs[user_id] = curr_sync + 1

Expand Down Expand Up @@ -565,7 +580,6 @@ async def set_state(
self,
target_user: UserID,
state: JsonDict,
ignore_status_msg: bool = False,
force_notify: bool = False,
) -> None:
"""Set the presence state of the user.
Expand Down Expand Up @@ -602,7 +616,6 @@ async def set_state(
instance_name=self._presence_writer_instance,
user_id=user_id,
state=state,
ignore_status_msg=ignore_status_msg,
force_notify=force_notify,
)

Expand Down Expand Up @@ -942,7 +955,10 @@ async def bump_presence_active_time(self, user: UserID) -> None:
await self._update_states([prev_state.copy_and_replace(**new_fields)])

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]:
"""Returns a context manager that should surround any stream requests
from the user.
Expand All @@ -956,6 +972,7 @@ async def user_syncing(
affect_presence: If false this function will be a no-op.
Useful for streams that are not associated with an actual
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
# disabled.
Expand All @@ -967,13 +984,14 @@ async def user_syncing(
self.user_to_num_current_syncs[user_id] = curr_sync + 1

prev_state = await self.current_state_for_user(user_id)
if prev_state.state == PresenceState.OFFLINE:
# If they're currently offline then bring them online, otherwise
# just update the last sync times.
if prev_state.state != PresenceState.BUSY:
# If they're busy then they don't stop being busy just by syncing,
# so just update the last sync & last active times. Otherwise, set the
# new presence value
await self._update_states(
[
prev_state.copy_and_replace(
state=PresenceState.ONLINE,
state=presence_state,
dbkr marked this conversation as resolved.
Show resolved Hide resolved
last_active_ts=self.clock.time_msec(),
last_user_sync_ts=self.clock.time_msec(),
)
Expand All @@ -983,7 +1001,8 @@ async def user_syncing(
await self._update_states(
[
prev_state.copy_and_replace(
last_user_sync_ts=self.clock.time_msec()
last_active_ts=self.clock.time_msec(),
last_user_sync_ts=self.clock.time_msec(),
dbkr marked this conversation as resolved.
Show resolved Hide resolved
)
]
)
squahtx marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -1086,11 +1105,6 @@ async def update_external_syncs_clear(self, process_id: str) -> 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:
"""Persist states in the database, poke the notifier and send to
interested remote servers
Expand Down Expand Up @@ -1166,7 +1180,6 @@ async def set_state(
self,
target_user: UserID,
state: JsonDict,
ignore_status_msg: bool = False,
force_notify: bool = False,
) -> None:
"""Set the presence state of the user.
Expand Down Expand Up @@ -1198,9 +1211,7 @@ async def set_state(
prev_state = await self.current_state_for_user(user_id)

new_fields = {"state": presence}

if not ignore_status_msg:
new_fields["status_msg"] = status_msg
new_fields["status_msg"] = status_msg

if presence == PresenceState.ONLINE or (
presence == PresenceState.BUSY and self._busy_presence_enabled
Expand Down
2 changes: 0 additions & 2 deletions synapse/replication/http/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ async def _serialize_payload( # type: ignore[override]
) -> JsonDict:
return {
"state": state,
"ignore_status_msg": ignore_status_msg,
"force_notify": force_notify,
}

Expand All @@ -114,7 +113,6 @@ async def _handle_request( # type: ignore[override]
await self._presence_handler.set_state(
UserID.from_string(user_id),
content["state"],
content["ignore_status_msg"],
squahtx marked this conversation as resolved.
Show resolved Hide resolved
content["force_notify"],
)

Expand Down
9 changes: 3 additions & 6 deletions synapse/rest/client/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,10 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:

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(
user.to_string(), affect_presence=affect_presence
user.to_string(),
affect_presence=affect_presence,
presence_state=set_presence,
)
with context:
sync_result = await self.sync_handler.wait_for_sync_for_user(
Expand Down
79 changes: 79 additions & 0 deletions tests/handlers/test_presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,85 @@ def test_set_presence_with_status_msg_none(self):
# Mark user as online and `status_msg = 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(
self, user_id: str, state: str, status_msg: Optional[str]
):
Expand Down