-
Notifications
You must be signed in to change notification settings - Fork 201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MSC4171 Omit service members from room summary #17866
Changes from 21 commits
ec91bea
027a86c
587b687
d9c52b2
656996a
21c4677
7e6ae4f
b13a032
b84e949
990e2ce
9567c14
0ab5dae
d5530ff
0a8d85b
1da2f61
5b8cbf4
e60d4cf
5a3cdf2
6c17ee1
7521b78
429381a
ee13e3f
2c181a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add experimental support for filtering out "service members" from room summary responses, as described in [MSC4171](https://github.com/matrix-org/matrix-spec-proposals/pull/4171). |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -285,7 +285,9 @@ def _get_users_in_room_with_profiles( | |
) | ||
|
||
@cached(max_entries=100000) # type: ignore[synapse-@cached-mutable] | ||
async def get_room_summary(self, room_id: str) -> Mapping[str, MemberSummary]: | ||
async def get_room_summary( | ||
self, room_id: str, exclude_service_users: bool = False | ||
) -> Mapping[str, MemberSummary]: | ||
""" | ||
Get the details of a room roughly suitable for use by the room | ||
summary extension to /sync. Useful when lazy loading room members. | ||
|
@@ -301,12 +303,13 @@ async def get_room_summary(self, room_id: str) -> Mapping[str, MemberSummary]: | |
|
||
Args: | ||
room_id: The room ID to query | ||
exclude_service_users: Should MSC4171 be used to exclude service members | ||
Returns: | ||
dict of membership states, pointing to a MemberSummary named tuple. | ||
""" | ||
|
||
def _get_room_summary_txn( | ||
txn: LoggingTransaction, | ||
txn: LoggingTransaction, exclude_members: List[str] | ||
) -> Dict[str, MemberSummary]: | ||
# first get counts. | ||
# We do this all in one transaction to keep the cache small. | ||
|
@@ -318,6 +321,10 @@ def _get_room_summary_txn( | |
for membership, count in counts.items(): | ||
res.setdefault(membership, MemberSummary([], count)) | ||
|
||
exclude_users_clause, args = make_in_list_sql_clause( | ||
self.database_engine, "state_key", exclude_members, negative=True | ||
) | ||
|
||
# Order by membership (joins -> invites -> leave (former insiders) -> | ||
# everything else (outsiders like bans/knocks), then by `stream_ordering` so | ||
# the first members in the room show up first and to make the sort stable | ||
|
@@ -330,16 +337,18 @@ def _get_room_summary_txn( | |
FROM current_state_events | ||
WHERE type = 'm.room.member' AND room_id = ? | ||
AND membership IS NOT NULL | ||
AND %s | ||
ORDER BY | ||
CASE membership WHEN ? THEN 1 WHEN ? THEN 2 WHEN ? THEN 3 ELSE 4 END ASC, | ||
event_stream_ordering ASC | ||
LIMIT ? | ||
""" | ||
""" % (exclude_users_clause) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use The old |
||
|
||
txn.execute( | ||
sql, | ||
( | ||
room_id, | ||
*args, | ||
# Sort order | ||
Membership.JOIN, | ||
Membership.INVITE, | ||
|
@@ -357,8 +366,33 @@ def _get_room_summary_txn( | |
|
||
return res | ||
|
||
exclude_members = [] | ||
if exclude_service_users: | ||
functional_members_event_id = await self.db_pool.simple_select_one_onecol( | ||
table="current_state_events", | ||
keyvalues={ | ||
"room_id": room_id, | ||
"type": EventTypes.MSC4171FunctionalMembers, | ||
"state_key": "", | ||
}, | ||
retcol="event_id", | ||
allow_none=True, | ||
) | ||
if functional_members_event_id: | ||
functional_members_event = await self.get_event( | ||
functional_members_event_id | ||
) | ||
functional_members_data = functional_members_event.content.get( | ||
"service_members" | ||
) | ||
# ONLY use this value if this looks like a valid list of strings. Otherwise, ignore. | ||
if isinstance(functional_members_data, list) and all( | ||
isinstance(item, str) for item in functional_members_data | ||
): | ||
exclude_members = functional_members_data | ||
|
||
return await self.db_pool.runInteraction( | ||
"get_room_summary", _get_room_summary_txn | ||
"get_room_summary", _get_room_summary_txn, exclude_members | ||
) | ||
|
||
@cached() | ||
|
@@ -1754,7 +1788,8 @@ def __init__( | |
|
||
|
||
def extract_heroes_from_room_summary( | ||
details: Mapping[str, MemberSummary], me: str | ||
details: Mapping[str, MemberSummary], | ||
me: str, | ||
) -> List[str]: | ||
"""Determine the users that represent a room, from the perspective of the `me` user. | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be careful about there being too many
exclude_members
and bailing to prevent DoS.Alternatively, we could just fetch N extra members like we do in case one of them is the calling user and do the exclusion outside of the SQL. But we should probably have a limit on that as well. Perhaps something to clarify in the spec and then we can bail if the list is longer than the specc'ed max. Perhaps we just rely on max length of an event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to rely on the max length of an event, which I think even if you had a long list of very short userIds would still only be up to about 9k or so (once you've included the usual event padding).
I'm not quite sure on the performance cost here, but I'd assume that a 9k string list filter in postgres isn't terrible as it's not going to impact IO.
Alternatively we could do as you say and set a sensible max number of users in the spec (say 100 or so). I'm generally a bit allergic to limitations in the spec, as someones probably going to come up with a use case of 101 members however it might be justified in the case of performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and
get_room_summary
has big fat cache on it so it's probably okay to do a slightly more expensive call here. Admittedly this does impact the hot path of sync, but I think the operation of pulling out excluded users is fairly fast.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We tend to limit
make_in_list_sql_clause(...)
to at-most 1000 (see usages ofbatch_iter(..., 1000)
) but it's a bit tough to do here with thenegative=True
condition.We should at-least add a comment here that we are assuming that there should be no more than 9.3k members to exclude based on the max length of an event (65535 bytes).
@m:h
) maximally@m:h.io
)@mm:h.io
)We could have a valid MXID check and add them to a
Set
to deduplicate but I don't think it's worth the extra computation.Perhaps we should just have a practical limit to re-evaluate if someone hits it. Have a 1k check and set
exclude_members = []
in that case with a log warning (warning instead of assert because we don't want to break the whole/sync
response). This can always be increased in the future when someone has a practical use case but avoids rooms where there only goal is performance abuse.