19
19
#
20
20
import logging
21
21
from itertools import chain
22
- from typing import TYPE_CHECKING , Any , Dict , Final , List , Optional , Set , Tuple
22
+ from typing import TYPE_CHECKING , Any , Dict , Final , List , Mapping , Optional , Set , Tuple
23
23
24
24
import attr
25
25
from immutabledict import immutabledict
28
28
from synapse .events import EventBase
29
29
from synapse .events .utils import strip_event
30
30
from synapse .handlers .relations import BundledAggregations
31
+ from synapse .storage .databases .main .roommember import extract_heroes_from_room_summary
31
32
from synapse .storage .databases .main .stream import CurrentStateDeltaMembership
33
+ from synapse .storage .roommember import MemberSummary
32
34
from synapse .types import (
33
35
JsonDict ,
34
36
PersistedEventPosition ,
@@ -1043,6 +1045,103 @@ async def sort_rooms(
1043
1045
reverse = True ,
1044
1046
)
1045
1047
1048
+ async def get_current_state_ids_at (
1049
+ self ,
1050
+ room_id : str ,
1051
+ room_membership_for_user_at_to_token : _RoomMembershipForUser ,
1052
+ state_filter : StateFilter ,
1053
+ to_token : StreamToken ,
1054
+ ) -> StateMap [str ]:
1055
+ """
1056
+ Get current state IDs for the user in the room according to their membership. This
1057
+ will be the current state at the time of their LEAVE/BAN, otherwise will be the
1058
+ current state <= to_token.
1059
+
1060
+ Args:
1061
+ room_id: The room ID to fetch data for
1062
+ room_membership_for_user_at_token: Membership information for the user
1063
+ in the room at the time of `to_token`.
1064
+ to_token: The point in the stream to sync up to.
1065
+ """
1066
+
1067
+ room_state_ids : StateMap [str ]
1068
+ # People shouldn't see past their leave/ban event
1069
+ if room_membership_for_user_at_to_token .membership in (
1070
+ Membership .LEAVE ,
1071
+ Membership .BAN ,
1072
+ ):
1073
+ # TODO: `get_state_ids_at(...)` doesn't take into account the "current state"
1074
+ room_state_ids = await self .storage_controllers .state .get_state_ids_at (
1075
+ room_id ,
1076
+ stream_position = to_token .copy_and_replace (
1077
+ StreamKeyType .ROOM ,
1078
+ room_membership_for_user_at_to_token .event_pos .to_room_stream_token (),
1079
+ ),
1080
+ state_filter = state_filter ,
1081
+ # Partially-stated rooms should have all state events except for
1082
+ # remote membership events. Since we've already excluded
1083
+ # partially-stated rooms unless `required_state` only has
1084
+ # `["m.room.member", "$LAZY"]` for membership, we should be able to
1085
+ # retrieve everything requested. When we're lazy-loading, if there
1086
+ # are some remote senders in the timeline, we should also have their
1087
+ # membership event because we had to auth that timeline event. Plus
1088
+ # we don't want to block the whole sync waiting for this one room.
1089
+ await_full_state = False ,
1090
+ )
1091
+ # Otherwise, we can get the latest current state in the room
1092
+ else :
1093
+ room_state_ids = await self .storage_controllers .state .get_current_state_ids (
1094
+ room_id ,
1095
+ state_filter ,
1096
+ # Partially-stated rooms should have all state events except for
1097
+ # remote membership events. Since we've already excluded
1098
+ # partially-stated rooms unless `required_state` only has
1099
+ # `["m.room.member", "$LAZY"]` for membership, we should be able to
1100
+ # retrieve everything requested. When we're lazy-loading, if there
1101
+ # are some remote senders in the timeline, we should also have their
1102
+ # membership event because we had to auth that timeline event. Plus
1103
+ # we don't want to block the whole sync waiting for this one room.
1104
+ await_full_state = False ,
1105
+ )
1106
+ # TODO: Query `current_state_delta_stream` and reverse/rewind back to the `to_token`
1107
+
1108
+ return room_state_ids
1109
+
1110
+ async def get_current_state_at (
1111
+ self ,
1112
+ room_id : str ,
1113
+ room_membership_for_user_at_to_token : _RoomMembershipForUser ,
1114
+ state_filter : StateFilter ,
1115
+ to_token : StreamToken ,
1116
+ ) -> StateMap [EventBase ]:
1117
+ """
1118
+ Get current state for the user in the room according to their membership. This
1119
+ will be the current state at the time of their LEAVE/BAN, otherwise will be the
1120
+ current state <= to_token.
1121
+
1122
+ Args:
1123
+ room_id: The room ID to fetch data for
1124
+ room_membership_for_user_at_token: Membership information for the user
1125
+ in the room at the time of `to_token`.
1126
+ to_token: The point in the stream to sync up to.
1127
+ """
1128
+ room_state_ids = await self .get_current_state_ids_at (
1129
+ room_id = room_id ,
1130
+ room_membership_for_user_at_to_token = room_membership_for_user_at_to_token ,
1131
+ state_filter = state_filter ,
1132
+ to_token = to_token ,
1133
+ )
1134
+
1135
+ event_map = await self .store .get_events (list (room_state_ids .values ()))
1136
+
1137
+ state_map = {}
1138
+ for key , event_id in room_state_ids .items ():
1139
+ event = event_map .get (event_id )
1140
+ if event :
1141
+ state_map [key ] = event
1142
+
1143
+ return state_map
1144
+
1046
1145
async def get_room_sync_data (
1047
1146
self ,
1048
1147
user : UserID ,
@@ -1074,7 +1173,7 @@ async def get_room_sync_data(
1074
1173
# membership. Currently, we have to make all of these optional because
1075
1174
# `invite`/`knock` rooms only have `stripped_state`. See
1076
1175
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
1077
- timeline_events : Optional [ List [EventBase ]] = None
1176
+ timeline_events : List [EventBase ] = []
1078
1177
bundled_aggregations : Optional [Dict [str , BundledAggregations ]] = None
1079
1178
limited : Optional [bool ] = None
1080
1179
prev_batch_token : Optional [StreamToken ] = None
@@ -1206,7 +1305,7 @@ async def get_room_sync_data(
1206
1305
1207
1306
# Figure out any stripped state events for invite/knocks. This allows the
1208
1307
# potential joiner to identify the room.
1209
- stripped_state : Optional [ List [JsonDict ]] = None
1308
+ stripped_state : List [JsonDict ] = []
1210
1309
if room_membership_for_user_at_to_token .membership in (
1211
1310
Membership .INVITE ,
1212
1311
Membership .KNOCK ,
@@ -1243,6 +1342,44 @@ async def get_room_sync_data(
1243
1342
# updates.
1244
1343
initial = True
1245
1344
1345
+ # Check whether the room has a name set
1346
+ name_state_ids = await self .get_current_state_ids_at (
1347
+ room_id = room_id ,
1348
+ room_membership_for_user_at_to_token = room_membership_for_user_at_to_token ,
1349
+ state_filter = StateFilter .from_types ([(EventTypes .Name , "" )]),
1350
+ to_token = to_token ,
1351
+ )
1352
+ name_event_id = name_state_ids .get ((EventTypes .Name , "" ))
1353
+
1354
+ room_membership_summary : Mapping [str , MemberSummary ]
1355
+ empty_membership_summary = MemberSummary ([], 0 )
1356
+ if room_membership_for_user_at_to_token .membership in (
1357
+ Membership .LEAVE ,
1358
+ Membership .BAN ,
1359
+ ):
1360
+ # TODO: Figure out how to get the membership summary for left/banned rooms
1361
+ room_membership_summary = {}
1362
+ else :
1363
+ room_membership_summary = await self .store .get_room_summary (room_id )
1364
+ # TODO: Reverse/rewind back to the `to_token`
1365
+
1366
+ # `heroes` are required if the room name is not set.
1367
+ #
1368
+ # Note: When you're the first one on your server to be invited to a new room
1369
+ # over federation, we only have access to some stripped state in
1370
+ # `event.unsigned.invite_room_state` which currently doesn't include `heroes`,
1371
+ # see https://github.com/matrix-org/matrix-spec/issues/380. This means that
1372
+ # clients won't be able to calculate the room name when necessary and just a
1373
+ # pitfall we have to deal with until that spec issue is resolved.
1374
+ hero_user_ids : List [str ] = []
1375
+ # TODO: Should we also check for `EventTypes.CanonicalAlias`
1376
+ # (`m.room.canonical_alias`) as a fallback for the room name? see
1377
+ # https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1671260153
1378
+ if name_event_id is None :
1379
+ hero_user_ids = extract_heroes_from_room_summary (
1380
+ room_membership_summary , me = user .to_string ()
1381
+ )
1382
+
1246
1383
# Fetch the `required_state` for the room
1247
1384
#
1248
1385
# No `required_state` for invite/knock rooms (just `stripped_state`)
@@ -1253,13 +1390,11 @@ async def get_room_sync_data(
1253
1390
# https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1653045932
1254
1391
#
1255
1392
# Calculate the `StateFilter` based on the `required_state` for the room
1256
- room_state : Optional [StateMap [EventBase ]] = None
1257
- required_room_state : Optional [StateMap [EventBase ]] = None
1393
+ required_state_filter = StateFilter .none ()
1258
1394
if room_membership_for_user_at_to_token .membership not in (
1259
1395
Membership .INVITE ,
1260
1396
Membership .KNOCK ,
1261
1397
):
1262
- required_state_filter = StateFilter .none ()
1263
1398
# If we have a double wildcard ("*", "*") in the `required_state`, we need
1264
1399
# to fetch all state for the room
1265
1400
#
@@ -1325,86 +1460,65 @@ async def get_room_sync_data(
1325
1460
1326
1461
required_state_filter = StateFilter .from_types (required_state_types )
1327
1462
1328
- # We need this base set of info for the response so let's just fetch it along
1329
- # with the `required_state` for the room
1330
- META_ROOM_STATE = [(EventTypes .Name , "" ), (EventTypes .RoomAvatar , "" )]
1463
+ # We need this base set of info for the response so let's just fetch it along
1464
+ # with the `required_state` for the room
1465
+ meta_room_state = [(EventTypes .Name , "" ), (EventTypes .RoomAvatar , "" )] + [
1466
+ (EventTypes .Member , hero_user_id ) for hero_user_id in hero_user_ids
1467
+ ]
1468
+ state_filter = StateFilter .all ()
1469
+ if required_state_filter != StateFilter .all ():
1331
1470
state_filter = StateFilter (
1332
1471
types = StateFilter .from_types (
1333
- chain (META_ROOM_STATE , required_state_filter .to_types ())
1472
+ chain (meta_room_state , required_state_filter .to_types ())
1334
1473
).types ,
1335
1474
include_others = required_state_filter .include_others ,
1336
1475
)
1337
1476
1338
- # We can return all of the state that was requested if this was the first
1339
- # time we've sent the room down this connection.
1340
- if initial :
1341
- # People shouldn't see past their leave/ban event
1342
- if room_membership_for_user_at_to_token .membership in (
1343
- Membership .LEAVE ,
1344
- Membership .BAN ,
1345
- ):
1346
- room_state = await self .storage_controllers .state .get_state_at (
1347
- room_id ,
1348
- stream_position = to_token .copy_and_replace (
1349
- StreamKeyType .ROOM ,
1350
- room_membership_for_user_at_to_token .event_pos .to_room_stream_token (),
1351
- ),
1352
- state_filter = state_filter ,
1353
- # Partially-stated rooms should have all state events except for
1354
- # remote membership events. Since we've already excluded
1355
- # partially-stated rooms unless `required_state` only has
1356
- # `["m.room.member", "$LAZY"]` for membership, we should be able to
1357
- # retrieve everything requested. When we're lazy-loading, if there
1358
- # are some remote senders in the timeline, we should also have their
1359
- # membership event because we had to auth that timeline event. Plus
1360
- # we don't want to block the whole sync waiting for this one room.
1361
- await_full_state = False ,
1362
- )
1363
- # Otherwise, we can get the latest current state in the room
1364
- else :
1365
- room_state = await self .storage_controllers .state .get_current_state (
1366
- room_id ,
1367
- state_filter ,
1368
- # Partially-stated rooms should have all state events except for
1369
- # remote membership events. Since we've already excluded
1370
- # partially-stated rooms unless `required_state` only has
1371
- # `["m.room.member", "$LAZY"]` for membership, we should be able to
1372
- # retrieve everything requested. When we're lazy-loading, if there
1373
- # are some remote senders in the timeline, we should also have their
1374
- # membership event because we had to auth that timeline event. Plus
1375
- # we don't want to block the whole sync waiting for this one room.
1376
- await_full_state = False ,
1377
- )
1378
- # TODO: Query `current_state_delta_stream` and reverse/rewind back to the `to_token`
1379
- else :
1380
- # TODO: Once we can figure out if we've sent a room down this connection before,
1381
- # we can return updates instead of the full required state.
1382
- raise NotImplementedError ()
1477
+ # We can return all of the state that was requested if this was the first
1478
+ # time we've sent the room down this connection.
1479
+ room_state : StateMap [EventBase ] = {}
1480
+ if initial :
1481
+ room_state = await self .get_current_state_at (
1482
+ room_id = room_id ,
1483
+ room_membership_for_user_at_to_token = room_membership_for_user_at_to_token ,
1484
+ state_filter = state_filter ,
1485
+ to_token = to_token ,
1486
+ )
1487
+ else :
1488
+ # TODO: Once we can figure out if we've sent a room down this connection before,
1489
+ # we can return updates instead of the full required state.
1490
+ raise NotImplementedError ()
1383
1491
1384
- if required_state_filter != StateFilter .none ():
1385
- required_room_state = required_state_filter .filter_state (room_state )
1492
+ required_room_state : StateMap [EventBase ] = {}
1493
+ if required_state_filter != StateFilter .none ():
1494
+ required_room_state = required_state_filter .filter_state (room_state )
1386
1495
1387
1496
# Find the room name and avatar from the state
1388
1497
room_name : Optional [str ] = None
1498
+ # TODO: Should we also check for `EventTypes.CanonicalAlias`
1499
+ # (`m.room.canonical_alias`) as a fallback for the room name? see
1500
+ # https://github.com/matrix-org/matrix-spec-proposals/pull/3575#discussion_r1671260153
1501
+ name_event = room_state .get ((EventTypes .Name , "" ))
1502
+ if name_event is not None :
1503
+ room_name = name_event .content .get ("name" )
1504
+
1389
1505
room_avatar : Optional [str ] = None
1390
- if room_state is not None :
1391
- name_event = room_state .get ((EventTypes .Name , "" ))
1392
- if name_event is not None :
1393
- room_name = name_event .content .get ("name" )
1394
-
1395
- avatar_event = room_state .get ((EventTypes .RoomAvatar , "" ))
1396
- if avatar_event is not None :
1397
- room_avatar = avatar_event .content .get ("url" )
1398
- elif stripped_state is not None :
1399
- for event in stripped_state :
1400
- if event ["type" ] == EventTypes .Name :
1401
- room_name = event .get ("content" , {}).get ("name" )
1402
- elif event ["type" ] == EventTypes .RoomAvatar :
1403
- room_avatar = event .get ("content" , {}).get ("url" )
1404
-
1405
- # Found everything so we can stop looking
1406
- if room_name is not None and room_avatar is not None :
1407
- break
1506
+ avatar_event = room_state .get ((EventTypes .RoomAvatar , "" ))
1507
+ if avatar_event is not None :
1508
+ room_avatar = avatar_event .content .get ("url" )
1509
+
1510
+ # Assemble heroes: extract the info from the state we just fetched
1511
+ heroes : List [SlidingSyncResult .RoomResult .StrippedHero ] = []
1512
+ for hero_user_id in hero_user_ids :
1513
+ member_event = room_state .get ((EventTypes .Member , hero_user_id ))
1514
+ if member_event is not None :
1515
+ heroes .append (
1516
+ SlidingSyncResult .RoomResult .StrippedHero (
1517
+ user_id = hero_user_id ,
1518
+ display_name = member_event .content .get ("displayname" ),
1519
+ avatar_url = member_event .content .get ("avatar_url" ),
1520
+ )
1521
+ )
1408
1522
1409
1523
# Figure out the last bump event in the room
1410
1524
last_bump_event_result = (
@@ -1423,24 +1537,24 @@ async def get_room_sync_data(
1423
1537
return SlidingSyncResult .RoomResult (
1424
1538
name = room_name ,
1425
1539
avatar = room_avatar ,
1426
- # TODO: Dummy value
1427
- heroes = None ,
1540
+ heroes = heroes ,
1428
1541
# TODO: Dummy value
1429
1542
is_dm = False ,
1430
1543
initial = initial ,
1431
- required_state = (
1432
- list (required_room_state .values ()) if required_room_state else None
1433
- ),
1544
+ required_state = list (required_room_state .values ()),
1434
1545
timeline_events = timeline_events ,
1435
1546
bundled_aggregations = bundled_aggregations ,
1436
1547
stripped_state = stripped_state ,
1437
1548
prev_batch = prev_batch_token ,
1438
1549
limited = limited ,
1439
1550
num_live = num_live ,
1440
1551
bump_stamp = bump_stamp ,
1441
- # TODO: Dummy values
1442
- joined_count = 0 ,
1443
- invited_count = 0 ,
1552
+ joined_count = room_membership_summary .get (
1553
+ Membership .JOIN , empty_membership_summary
1554
+ ).count ,
1555
+ invited_count = room_membership_summary .get (
1556
+ Membership .INVITE , empty_membership_summary
1557
+ ).count ,
1444
1558
# TODO: These are just dummy values. We could potentially just remove these
1445
1559
# since notifications can only really be done correctly on the client anyway
1446
1560
# (encrypted rooms).
0 commit comments