49
49
import synapse .metrics
50
50
from synapse .api .constants import EduTypes , EventTypes , Membership , PresenceState
51
51
from synapse .api .errors import SynapseError
52
- from synapse .api .presence import UserPresenceState
52
+ from synapse .api .presence import UserDevicePresenceState , UserPresenceState
53
53
from synapse .appservice import ApplicationService
54
54
from synapse .events .presence_router import PresenceRouter
55
55
from synapse .logging .context import run_in_background
@@ -150,11 +150,16 @@ def __init__(self, hs: "HomeServer"):
150
150
self ._busy_presence_enabled = hs .config .experimental .msc3026_enabled
151
151
152
152
active_presence = self .store .take_presence_startup_info ()
153
+ # The combine status across all user devices.
153
154
self .user_to_current_state = {state .user_id : state for state in active_presence }
154
155
155
156
@abc .abstractmethod
156
157
async def user_syncing (
157
- self , user_id : str , affect_presence : bool , presence_state : str
158
+ self ,
159
+ user_id : str ,
160
+ device_id : Optional [str ],
161
+ affect_presence : bool ,
162
+ presence_state : str ,
158
163
) -> ContextManager [None ]:
159
164
"""Returns a context manager that should surround any stream requests
160
165
from the user.
@@ -241,6 +246,7 @@ async def current_state_for_user(self, user_id: str) -> UserPresenceState:
241
246
async def set_state (
242
247
self ,
243
248
target_user : UserID ,
249
+ device_id : Optional [str ],
244
250
state : JsonDict ,
245
251
ignore_status_msg : bool = False ,
246
252
force_notify : bool = False ,
@@ -368,7 +374,9 @@ async def send_full_presence_to_users(self, user_ids: StrCollection) -> None:
368
374
# We set force_notify=True here so that this presence update is guaranteed to
369
375
# increment the presence stream ID (which resending the current user's presence
370
376
# otherwise would not do).
371
- await self .set_state (UserID .from_string (user_id ), state , force_notify = True )
377
+ await self .set_state (
378
+ UserID .from_string (user_id ), None , state , force_notify = True
379
+ )
372
380
373
381
async def is_visible (self , observed_user : UserID , observer_user : UserID ) -> bool :
374
382
raise NotImplementedError (
@@ -472,7 +480,11 @@ def send_stop_syncing(self) -> None:
472
480
self .send_user_sync (user_id , False , last_sync_ms )
473
481
474
482
async def user_syncing (
475
- self , user_id : str , affect_presence : bool , presence_state : str
483
+ self ,
484
+ user_id : str ,
485
+ device_id : Optional [str ],
486
+ affect_presence : bool ,
487
+ presence_state : str ,
476
488
) -> ContextManager [None ]:
477
489
"""Record that a user is syncing.
478
490
@@ -490,7 +502,10 @@ async def user_syncing(
490
502
# what the spec wants: see comment in the BasePresenceHandler version
491
503
# of this function.
492
504
await self .set_state (
493
- UserID .from_string (user_id ), {"presence" : presence_state }, True
505
+ UserID .from_string (user_id ),
506
+ device_id ,
507
+ {"presence" : presence_state },
508
+ True ,
494
509
)
495
510
496
511
curr_sync = self ._user_to_num_current_syncs .get (user_id , 0 )
@@ -586,6 +601,7 @@ def get_currently_syncing_users_for_replication(self) -> Iterable[str]:
586
601
async def set_state (
587
602
self ,
588
603
target_user : UserID ,
604
+ device_id : Optional [str ],
589
605
state : JsonDict ,
590
606
ignore_status_msg : bool = False ,
591
607
force_notify : bool = False ,
@@ -623,6 +639,7 @@ async def set_state(
623
639
await self ._set_state_client (
624
640
instance_name = self ._presence_writer_instance ,
625
641
user_id = user_id ,
642
+ device_id = device_id ,
626
643
state = state ,
627
644
ignore_status_msg = ignore_status_msg ,
628
645
force_notify = force_notify ,
@@ -755,6 +772,11 @@ def run_persister() -> Awaitable[None]:
755
772
self ._event_pos = self .store .get_room_max_stream_ordering ()
756
773
self ._event_processing = False
757
774
775
+ # The per-device presence state, maps user to devices to per-device presence state.
776
+ self .user_to_device_to_current_state : Dict [
777
+ str , Dict [Optional [str ], UserDevicePresenceState ]
778
+ ] = {}
779
+
758
780
async def _on_shutdown (self ) -> None :
759
781
"""Gets called when shutting down. This lets us persist any updates that
760
782
we haven't yet persisted, e.g. updates that only changes some internal
@@ -973,6 +995,7 @@ async def bump_presence_active_time(self, user: UserID) -> None:
973
995
async def user_syncing (
974
996
self ,
975
997
user_id : str ,
998
+ device_id : Optional [str ],
976
999
affect_presence : bool = True ,
977
1000
presence_state : str = PresenceState .ONLINE ,
978
1001
) -> ContextManager [None ]:
@@ -985,6 +1008,7 @@ async def user_syncing(
985
1008
986
1009
Args:
987
1010
user_id
1011
+ device_id
988
1012
affect_presence: If false this function will be a no-op.
989
1013
Useful for streams that are not associated with an actual
990
1014
client that is being used by a user.
@@ -1010,7 +1034,10 @@ async def user_syncing(
1010
1034
# updated always, which is not what the spec calls for, but synapse has done
1011
1035
# this for... forever, I think.
1012
1036
await self .set_state (
1013
- UserID .from_string (user_id ), {"presence" : presence_state }, True
1037
+ UserID .from_string (user_id ),
1038
+ device_id ,
1039
+ {"presence" : presence_state },
1040
+ True ,
1014
1041
)
1015
1042
# Retrieve the new state for the logic below. This should come from the
1016
1043
# in-memory cache.
@@ -1213,6 +1240,7 @@ async def incoming_presence(self, origin: str, content: JsonDict) -> None:
1213
1240
async def set_state (
1214
1241
self ,
1215
1242
target_user : UserID ,
1243
+ device_id : Optional [str ],
1216
1244
state : JsonDict ,
1217
1245
ignore_status_msg : bool = False ,
1218
1246
force_notify : bool = False ,
@@ -1221,6 +1249,7 @@ async def set_state(
1221
1249
1222
1250
Args:
1223
1251
target_user: The ID of the user to set the presence state of.
1252
+ device_id: The optional device ID.
1224
1253
state: The presence state as a JSON dictionary.
1225
1254
ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
1226
1255
If False, the user's current status will be updated.
@@ -1249,6 +1278,42 @@ async def set_state(
1249
1278
1250
1279
prev_state = await self .current_state_for_user (user_id )
1251
1280
1281
+ # Always update the device specific information.
1282
+ # Get the previous device state.
1283
+ device_state = self .user_to_device_to_current_state .setdefault (
1284
+ user_id , {}
1285
+ ).setdefault (
1286
+ device_id ,
1287
+ UserDevicePresenceState (
1288
+ user_id ,
1289
+ device_id ,
1290
+ presence ,
1291
+ last_active_ts = self .clock .time_msec (),
1292
+ last_user_sync_ts = self .clock .time_msec (),
1293
+ status_msg = None ,
1294
+ ),
1295
+ )
1296
+ device_state .state = presence
1297
+ if presence :
1298
+ device_state .status_msg = status_msg
1299
+ device_state .last_active_ts = self .clock .time_msec ()
1300
+ device_state .last_user_sync_ts = self .clock .time_msec ()
1301
+
1302
+ # Based on (all) the user's devices calculate the new presence state.
1303
+ presence_by_priority = {
1304
+ PresenceState .BUSY : 4 ,
1305
+ PresenceState .ONLINE : 3 ,
1306
+ PresenceState .UNAVAILABLE : 2 ,
1307
+ PresenceState .OFFLINE : 1 ,
1308
+ }
1309
+ for device_state in self .user_to_device_to_current_state [user_id ].values ():
1310
+ if (
1311
+ presence_by_priority [device_state .state ]
1312
+ > presence_by_priority [presence ]
1313
+ ):
1314
+ presence = device_state .state
1315
+
1316
+ # The newly updated status as an amalgomation of all the device statuses.
1252
1317
new_fields = {"state" : presence }
1253
1318
1254
1319
if not ignore_status_msg :
@@ -1962,6 +2027,7 @@ def handle_update(
1962
2027
# If the users are ours then we want to set up a bunch of timers
1963
2028
# to time things out.
1964
2029
if is_mine :
2030
+ # TODO Maybe don't do this if currently active?
1965
2031
if new_state .state == PresenceState .ONLINE :
1966
2032
# Idle timer
1967
2033
wheel_timer .insert (
0 commit comments