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

Add a new module API to update user presence state. #16544

Merged
merged 7 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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/16544.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a new module API for controller presence.
3 changes: 3 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ Example configuration:
presence:
enabled: false
```

The `enabled_for_sync` sub-option can be used to selectively enable/disable
clokep marked this conversation as resolved.
Show resolved Hide resolved
returning presence information in `/sync` response.
---
### `require_auth_for_profile_requests`

Expand Down
5 changes: 5 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,11 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
if self.use_presence is None:
self.use_presence = config.get("use_presence", True)

# Selectively enable syncing of presence, even if it is disabled.
self.use_presence_for_sync = presence_config.get(
"enabled_for_sync", self.use_presence
)

# Custom presence router module
# This is the legacy way of configuring it (the config should now be put in the modules section)
self.presence_router_module_class = None
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/initial_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ async def _room_initial_sync_joined(

async def get_presence() -> List[JsonDict]:
# If presence is disabled, return an empty list
if not self.hs.config.server.use_presence:
if not self.hs.config.server.use_presence_for_sync:
return []

states = await presence_handler.get_states(
Expand Down
46 changes: 29 additions & 17 deletions synapse/handlers/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,10 @@ async def _persist_unpersisted_changes(self) -> None:
)

async def _update_states(
self, new_states: Iterable[UserPresenceState], force_notify: bool = False
self,
new_states: Iterable[UserPresenceState],
force_notify: bool = False,
override: bool = False,
) -> None:
"""Updates presence of users. Sets the appropriate timeouts. Pokes
the notifier and federation if and only if the changed presence state
Expand All @@ -917,8 +920,9 @@ async def _update_states(
even if it doesn't change the state of a user's presence (e.g online -> online).
This is currently used to bump the max presence stream ID without changing any
user's presence (see PresenceHandler.add_users_to_send_full_presence_to).
override: Whether to set the presence state even if presence is disabled.
"""
if not self._presence_enabled:
if not self._presence_enabled and not override:
# We shouldn't get here if presence is disabled, but we check anyway
# to ensure that we don't a) send out presence federation and b)
# don't add things to the wheel timer that will never be handled.
Expand Down Expand Up @@ -957,6 +961,7 @@ async def _update_states(
is_mine=self.is_mine_id(user_id),
wheel_timer=self.wheel_timer,
now=now,
override=override,
)

if force_notify:
Expand Down Expand Up @@ -2118,6 +2123,7 @@ def handle_update(
is_mine: bool,
wheel_timer: WheelTimer,
now: int,
override: bool,
) -> Tuple[UserPresenceState, bool, bool]:
"""Given a presence update:
1. Add any appropriate timers.
Expand All @@ -2129,6 +2135,8 @@ def handle_update(
is_mine: Whether the user is ours
wheel_timer
now: Time now in ms
override: True if this state should persist until another update occurs.
clokep marked this conversation as resolved.
Show resolved Hide resolved
Skips insertion into wheel timers.

Returns:
3-tuple: `(new_state, persist_and_notify, federation_ping)` where:
Expand All @@ -2146,14 +2154,15 @@ def handle_update(
if is_mine:
if new_state.state == PresenceState.ONLINE:
# Idle timer
wheel_timer.insert(
now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER
)
if not override:
wheel_timer.insert(
now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER
)

active = now - new_state.last_active_ts < LAST_ACTIVE_GRANULARITY
new_state = new_state.copy_and_replace(currently_active=active)

if active:
if active and not override:
wheel_timer.insert(
now=now,
obj=user_id,
Expand All @@ -2162,31 +2171,34 @@ def handle_update(

if new_state.state != PresenceState.OFFLINE:
# User has stopped syncing
wheel_timer.insert(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
)
if not override:
wheel_timer.insert(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
)

last_federate = new_state.last_federation_update_ts
if now - last_federate > FEDERATION_PING_INTERVAL:
# Been a while since we've poked remote servers
new_state = new_state.copy_and_replace(last_federation_update_ts=now)
federation_ping = True

if new_state.state == PresenceState.BUSY:
if new_state.state == PresenceState.BUSY and not override:
wheel_timer.insert(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + BUSY_ONLINE_TIMEOUT,
)

else:
wheel_timer.insert(
now=now,
obj=user_id,
then=new_state.last_federation_update_ts + FEDERATION_TIMEOUT,
)
# An update for a remote user was received.
if not override:
wheel_timer.insert(
now=now,
obj=user_id,
then=new_state.last_federation_update_ts + FEDERATION_TIMEOUT,
)

# Check whether the change was something worth notifying about
if should_notify(prev_state, new_state, is_mine):
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -1512,7 +1512,7 @@ async def generate_sync_result(

# Presence data is included if the server has it enabled and not filtered out.
include_presence_data = bool(
self.hs_config.server.use_presence
self.hs_config.server.use_presence_for_sync
and not sync_config.filter_collection.blocks_all_presence()
)
# Device list updates are sent if a since token is provided.
Expand Down
35 changes: 35 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Generator,
Iterable,
List,
Mapping,
Optional,
Tuple,
TypeVar,
Expand All @@ -39,6 +40,7 @@

from synapse.api import errors
from synapse.api.errors import SynapseError
from synapse.api.presence import UserPresenceState
from synapse.config import ConfigError
from synapse.events import EventBase
from synapse.events.presence_router import (
Expand Down Expand Up @@ -1184,6 +1186,39 @@ async def send_local_online_presence_to(self, users: Iterable[str]) -> None:
presence_events, [destination]
)

async def set_presence_for_users(
self, users: Mapping[str, Tuple[str, Optional[str]]]
) -> None:
"""
Update the internal presence state of users.

Note that this can be used for either local or remote users.

Note that this method can only be run on the process that is configured to write to the
presence stream. By default, this is the main process.

Added in Synapse v1.96.0.
"""

# We pull out the presence handler here to break a cyclic
# dependency between the presence router and module API.
presence_handler = self._hs.get_presence_handler()

from synapse.handlers.presence import PresenceHandler

assert isinstance(presence_handler, PresenceHandler)

states = await presence_handler.current_state_for_users(users.keys())
for user_id, (state, status_msg) in users.items():
prev_state = states.setdefault(user_id, UserPresenceState.default(user_id))
states[user_id] = prev_state.copy_and_replace(
state=state, status_msg=status_msg
)

await presence_handler._update_states(
states.values(), force_notify=True, override=True
)

def looping_background_call(
self,
f: Callable,
Expand Down
86 changes: 78 additions & 8 deletions tests/handlers/test_presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import itertools
from typing import Optional, cast
from unittest.mock import Mock, call

Expand Down Expand Up @@ -66,7 +66,12 @@ def test_offline_to_online(self) -> None:
)

state, persist_and_notify, federation_ping = handle_update(
prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
prev_state,
new_state,
is_mine=True,
wheel_timer=wheel_timer,
now=now,
override=False,
)

self.assertTrue(persist_and_notify)
Expand Down Expand Up @@ -108,7 +113,12 @@ def test_online_to_online(self) -> None:
)

state, persist_and_notify, federation_ping = handle_update(
prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
prev_state,
new_state,
is_mine=True,
wheel_timer=wheel_timer,
now=now,
override=False,
)

self.assertFalse(persist_and_notify)
Expand Down Expand Up @@ -153,7 +163,12 @@ def test_online_to_online_last_active_noop(self) -> None:
)

state, persist_and_notify, federation_ping = handle_update(
prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
prev_state,
new_state,
is_mine=True,
wheel_timer=wheel_timer,
now=now,
override=False,
)

self.assertFalse(persist_and_notify)
Expand Down Expand Up @@ -196,7 +211,12 @@ def test_online_to_online_last_active(self) -> None:
new_state = prev_state.copy_and_replace(state=PresenceState.ONLINE)

state, persist_and_notify, federation_ping = handle_update(
prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
prev_state,
new_state,
is_mine=True,
wheel_timer=wheel_timer,
now=now,
override=False,
)

self.assertTrue(persist_and_notify)
Expand Down Expand Up @@ -231,7 +251,12 @@ def test_remote_ping_timer(self) -> None:
new_state = prev_state.copy_and_replace(state=PresenceState.ONLINE)

state, persist_and_notify, federation_ping = handle_update(
prev_state, new_state, is_mine=False, wheel_timer=wheel_timer, now=now
prev_state,
new_state,
is_mine=False,
wheel_timer=wheel_timer,
now=now,
override=False,
)

self.assertFalse(persist_and_notify)
Expand Down Expand Up @@ -265,7 +290,12 @@ def test_online_to_offline(self) -> None:
new_state = prev_state.copy_and_replace(state=PresenceState.OFFLINE)

state, persist_and_notify, federation_ping = handle_update(
prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
prev_state,
new_state,
is_mine=True,
wheel_timer=wheel_timer,
now=now,
override=False,
)

self.assertTrue(persist_and_notify)
Expand All @@ -287,7 +317,12 @@ def test_online_to_idle(self) -> None:
new_state = prev_state.copy_and_replace(state=PresenceState.UNAVAILABLE)

state, persist_and_notify, federation_ping = handle_update(
prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now
prev_state,
new_state,
is_mine=True,
wheel_timer=wheel_timer,
now=now,
override=False,
)

self.assertTrue(persist_and_notify)
Expand Down Expand Up @@ -347,6 +382,41 @@ def test_persisting_presence_updates(self) -> None:
# They should be identical.
self.assertEqual(presence_states_compare, db_presence_states)

@parameterized.expand(
itertools.permutations(
(
PresenceState.BUSY,
PresenceState.ONLINE,
PresenceState.UNAVAILABLE,
PresenceState.OFFLINE,
),
2,
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved
)
)
def test_override(self, initial_state: str, final_state: str) -> None:
"""Overridden statuses should not go into the wheel timer."""
wheel_timer = Mock()
user_id = "@foo:bar"
now = 5000000

prev_state = UserPresenceState.default(user_id)
prev_state = prev_state.copy_and_replace(
state=initial_state, last_active_ts=now, currently_active=True
)

new_state = prev_state.copy_and_replace(state=final_state, last_active_ts=now)

handle_update(
prev_state,
new_state,
is_mine=True,
wheel_timer=wheel_timer,
now=now,
override=True,
)

wheel_timer.insert.assert_not_called()


class PresenceTimeoutTestCase(unittest.TestCase):
"""Tests different timers and that the timer does not change `status_msg` of user."""
Expand Down
Loading