@@ -222,9 +222,21 @@ async def current_state_for_users(
222222
223223 @abc .abstractmethod
224224 async def set_state (
225- self , target_user : UserID , state : JsonDict , ignore_status_msg : bool = False
225+ self ,
226+ target_user : UserID ,
227+ state : JsonDict ,
228+ ignore_status_msg : bool = False ,
229+ force_notify : bool = False ,
226230 ) -> None :
227- """Set the presence state of the user. """
231+ """Set the presence state of the user.
232+
233+ Args:
234+ target_user: The ID of the user to set the presence state of.
235+ state: The presence state as a JSON dictionary.
236+ ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
237+ If False, the user's current status will be updated.
238+ force_notify: Whether to force notification of the update to clients.
239+ """
228240
229241 @abc .abstractmethod
230242 async def bump_presence_active_time (self , user : UserID ):
@@ -296,6 +308,51 @@ async def maybe_send_presence_to_interested_destinations(
296308 for destinations , states in hosts_and_states :
297309 self ._federation .send_presence_to_destinations (states , destinations )
298310
311+ async def send_full_presence_to_users (self , user_ids : Collection [str ]):
312+ """
313+ Adds to the list of users who should receive a full snapshot of presence
314+ upon their next sync. Note that this only works for local users.
315+
316+ Then, grabs the current presence state for a given set of users and adds it
317+ to the top of the presence stream.
318+
319+ Args:
320+ user_ids: The IDs of the local users to send full presence to.
321+ """
322+ # Retrieve one of the users from the given set
323+ if not user_ids :
324+ raise Exception (
325+ "send_full_presence_to_users must be called with at least one user"
326+ )
327+ user_id = next (iter (user_ids ))
328+
329+ # Mark all users as receiving full presence on their next sync
330+ await self .store .add_users_to_send_full_presence_to (user_ids )
331+
332+ # Add a new entry to the presence stream. Since we use stream tokens to determine whether a
333+ # local user should receive a full snapshot of presence when they sync, we need to bump the
334+ # presence stream so that subsequent syncs with no presence activity in between won't result
335+ # in the client receiving multiple full snapshots of presence.
336+ #
337+ # If we bump the stream ID, then the user will get a higher stream token next sync, and thus
338+ # correctly won't receive a second snapshot.
339+
340+ # Get the current presence state for one of the users (defaults to offline if not found)
341+ current_presence_state = await self .get_state (UserID .from_string (user_id ))
342+
343+ # Convert the UserPresenceState object into a serializable dict
344+ state = {
345+ "presence" : current_presence_state .state ,
346+ "status_message" : current_presence_state .status_msg ,
347+ }
348+
349+ # Copy the presence state to the tip of the presence stream.
350+
351+ # We set force_notify=True here so that this presence update is guaranteed to
352+ # increment the presence stream ID (which resending the current user's presence
353+ # otherwise would not do).
354+ await self .set_state (UserID .from_string (user_id ), state , force_notify = True )
355+
299356
300357class _NullContextManager (ContextManager [None ]):
301358 """A context manager which does nothing."""
@@ -480,8 +537,17 @@ async def set_state(
480537 target_user : UserID ,
481538 state : JsonDict ,
482539 ignore_status_msg : bool = False ,
540+ force_notify : bool = False ,
483541 ) -> None :
484- """Set the presence state of the user."""
542+ """Set the presence state of the user.
543+
544+ Args:
545+ target_user: The ID of the user to set the presence state of.
546+ state: The presence state as a JSON dictionary.
547+ ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
548+ If False, the user's current status will be updated.
549+ force_notify: Whether to force notification of the update to clients.
550+ """
485551 presence = state ["presence" ]
486552
487553 valid_presence = (
@@ -508,6 +574,7 @@ async def set_state(
508574 user_id = user_id ,
509575 state = state ,
510576 ignore_status_msg = ignore_status_msg ,
577+ force_notify = force_notify ,
511578 )
512579
513580 async def bump_presence_active_time (self , user : UserID ) -> None :
@@ -677,13 +744,19 @@ async def _persist_unpersisted_changes(self) -> None:
677744 [self .user_to_current_state [user_id ] for user_id in unpersisted ]
678745 )
679746
680- async def _update_states (self , new_states : Iterable [UserPresenceState ]) -> None :
747+ async def _update_states (
748+ self , new_states : Iterable [UserPresenceState ], force_notify : bool = False
749+ ) -> None :
681750 """Updates presence of users. Sets the appropriate timeouts. Pokes
682751 the notifier and federation if and only if the changed presence state
683752 should be sent to clients/servers.
684753
685754 Args:
686755 new_states: The new user presence state updates to process.
756+ force_notify: Whether to force notifying clients of this presence state update,
757+ even if it doesn't change the state of a user's presence (e.g online -> online).
758+ This is currently used to bump the max presence stream ID without changing any
759+ user's presence (see PresenceHandler.add_users_to_send_full_presence_to).
687760 """
688761 now = self .clock .time_msec ()
689762
@@ -720,6 +793,9 @@ async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None:
720793 now = now ,
721794 )
722795
796+ if force_notify :
797+ should_notify = True
798+
723799 self .user_to_current_state [user_id ] = new_state
724800
725801 if should_notify :
@@ -1058,9 +1134,21 @@ async def incoming_presence(self, origin: str, content: JsonDict) -> None:
10581134 await self ._update_states (updates )
10591135
10601136 async def set_state (
1061- self , target_user : UserID , state : JsonDict , ignore_status_msg : bool = False
1137+ self ,
1138+ target_user : UserID ,
1139+ state : JsonDict ,
1140+ ignore_status_msg : bool = False ,
1141+ force_notify : bool = False ,
10621142 ) -> None :
1063- """Set the presence state of the user."""
1143+ """Set the presence state of the user.
1144+
1145+ Args:
1146+ target_user: The ID of the user to set the presence state of.
1147+ state: The presence state as a JSON dictionary.
1148+ ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
1149+ If False, the user's current status will be updated.
1150+ force_notify: Whether to force notification of the update to clients.
1151+ """
10641152 status_msg = state .get ("status_msg" , None )
10651153 presence = state ["presence" ]
10661154
@@ -1091,7 +1179,9 @@ async def set_state(
10911179 ):
10921180 new_fields ["last_active_ts" ] = self .clock .time_msec ()
10931181
1094- await self ._update_states ([prev_state .copy_and_replace (** new_fields )])
1182+ await self ._update_states (
1183+ [prev_state .copy_and_replace (** new_fields )], force_notify = force_notify
1184+ )
10951185
10961186 async def is_visible (self , observed_user : UserID , observer_user : UserID ) -> bool :
10971187 """Returns whether a user can see another user's presence."""
@@ -1389,11 +1479,10 @@ def __init__(self, hs: "HomeServer"):
13891479 #
13901480 # Presence -> Notifier -> PresenceEventSource -> Presence
13911481 #
1392- # Same with get_module_api, get_presence_router
1482+ # Same with get_presence_router:
13931483 #
13941484 # AuthHandler -> Notifier -> PresenceEventSource -> ModuleApi -> AuthHandler
13951485 self .get_presence_handler = hs .get_presence_handler
1396- self .get_module_api = hs .get_module_api
13971486 self .get_presence_router = hs .get_presence_router
13981487 self .clock = hs .get_clock ()
13991488 self .store = hs .get_datastore ()
@@ -1424,16 +1513,21 @@ async def get_new_events(
14241513 stream_change_cache = self .store .presence_stream_cache
14251514
14261515 with Measure (self .clock , "presence.get_new_events" ):
1427- if user_id in self .get_module_api ()._send_full_presence_to_local_users :
1428- # This user has been specified by a module to receive all current, online
1429- # user presence. Removing from_key and setting include_offline to false
1430- # will do effectively this.
1431- from_key = None
1432- include_offline = False
1433-
14341516 if from_key is not None :
14351517 from_key = int (from_key )
14361518
1519+ # Check if this user should receive all current, online user presence. We only
1520+ # bother to do this if from_key is set, as otherwise the user will receive all
1521+ # user presence anyways.
1522+ if await self .store .should_user_receive_full_presence_with_token (
1523+ user_id , from_key
1524+ ):
1525+ # This user has been specified by a module to receive all current, online
1526+ # user presence. Removing from_key and setting include_offline to false
1527+ # will do effectively this.
1528+ from_key = None
1529+ include_offline = False
1530+
14371531 max_token = self .store .get_current_presence_token ()
14381532 if from_key == max_token :
14391533 # This is necessary as due to the way stream ID generators work
@@ -1467,12 +1561,6 @@ async def get_new_events(
14671561 user_id , include_offline , from_key
14681562 )
14691563
1470- # Remove the user from the list of users to receive all presence
1471- if user_id in self .get_module_api ()._send_full_presence_to_local_users :
1472- self .get_module_api ()._send_full_presence_to_local_users .remove (
1473- user_id
1474- )
1475-
14761564 return presence_updates , max_token
14771565
14781566 # Make mypy happy. users_interested_in should now be a set
@@ -1522,10 +1610,6 @@ async def get_new_events(
15221610 )
15231611 presence_updates = list (users_to_state .values ())
15241612
1525- # Remove the user from the list of users to receive all presence
1526- if user_id in self .get_module_api ()._send_full_presence_to_local_users :
1527- self .get_module_api ()._send_full_presence_to_local_users .remove (user_id )
1528-
15291613 if not include_offline :
15301614 # Filter out offline presence states
15311615 presence_updates = self ._filter_offline_presence_state (presence_updates )
0 commit comments