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

Move experimental features MSC3026, MSC3881, and MSC3967 to per-user flags #15345

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fea933f
move experimental feature msc3026 (busy presence) to per-user flag
H-Shay Mar 28, 2023
0d61d3d
move msc3967 (Do not require UIA when first uploading cross signing k…
H-Shay Mar 28, 2023
1739ce6
move experimental feature msc3881 (remotely toggle push) to per-user …
H-Shay Mar 28, 2023
4aea2de
move experimental feature msc2654 (unread counts) to per-user flag
H-Shay Mar 28, 2023
4291c66
newsfragment
H-Shay Mar 28, 2023
d3cc11d
forgot to lint
H-Shay Mar 28, 2023
ca3e15b
add experimental features store to worker store
H-Shay Mar 29, 2023
51769a9
re-add parameters to test
H-Shay Mar 29, 2023
f9e7a0a
add a db function to tell if just one feature is enabled
H-Shay May 1, 2023
842eb40
update tests to use enum
H-Shay May 1, 2023
e5f33c5
fall back to default config setting if not enabled in table
H-Shay May 2, 2023
15dd372
stupid github web editor
H-Shay May 2, 2023
e53a8a5
change how config is checked
H-Shay May 2, 2023
e8c571b
remove support for per-user msc2654
H-Shay May 2, 2023
aea7cbd
move ExperimentalFeature definition to avoid circular import
H-Shay May 10, 2023
e156b84
consolidate logic checking config and db to one place
H-Shay May 10, 2023
7568c72
test activation both by config and admin api
H-Shay May 10, 2023
9155b82
remove changes to sync
H-Shay May 10, 2023
2cb4182
Merge branch 'develop' into shay/experimental_flags_part_2
H-Shay May 10, 2023
a767f1c
small fix
H-Shay May 10, 2023
b99dab3
Merge branch 'shay/experimental_flags_part_2' of https://github.com/m…
H-Shay May 10, 2023
86ec834
Update changelog.d/15345.feature
H-Shay May 10, 2023
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
3 changes: 3 additions & 0 deletions changelog.d/15345.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Follow-up to adding experimental feature flags per-user (#15344) which moves experimental features MSC3026 (busy presence),
MSC2654 (unread counts), MSC3881 (remotely toggle push notifications for another client), and
MSC3967 (Do not require UIA when first uploading cross signing keys) from the experimental config to per-user flags.
2 changes: 2 additions & 0 deletions synapse/app/generic_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from synapse.rest.synapse.client import build_synapse_client_resource_tree
from synapse.rest.well_known import well_known_resource
from synapse.server import HomeServer
from synapse.storage.databases.main import ExperimentalFeaturesStore
from synapse.storage.databases.main.account_data import AccountDataWorkerStore
from synapse.storage.databases.main.appservice import (
ApplicationServiceTransactionWorkerStore,
Expand Down Expand Up @@ -146,6 +147,7 @@ class GenericWorkerSlavedStore(
TransactionWorkerStore,
LockStore,
SessionStore,
ExperimentalFeaturesStore,
):
# Properties that multiple storage classes define. Tell mypy what the
# expected type is.
Expand Down
22 changes: 15 additions & 7 deletions synapse/handlers/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,6 @@ def __init__(self, hs: "HomeServer"):

self._federation_queue = PresenceFederationQueue(hs, self)

self._busy_presence_enabled = hs.config.experimental.msc3026_enabled

active_presence = self.store.take_presence_startup_info()
self.user_to_current_state = {state.user_id: state for state in active_presence}

Expand Down Expand Up @@ -422,8 +420,6 @@ def __init__(self, hs: "HomeServer"):
self.send_stop_syncing, UPDATE_SYNCING_USERS_MS
)

self._busy_presence_enabled = hs.config.experimental.msc3026_enabled

hs.get_reactor().addSystemEventTrigger(
"before",
"shutdown",
Expand Down Expand Up @@ -609,8 +605,14 @@ async def set_state(
PresenceState.BUSY,
)

busy_presence_enabled = await self.hs.get_datastores().main.get_feature_enabled(
target_user.to_string(), "msc3026"
)
if not busy_presence_enabled:
busy_presence_enabled = self.hs.config.experimental.msc3026_enabled

if presence not in valid_presence or (
presence == PresenceState.BUSY and not self._busy_presence_enabled
presence == PresenceState.BUSY and not busy_presence_enabled
):
raise SynapseError(400, "Invalid presence state")

Expand Down Expand Up @@ -1238,8 +1240,14 @@ async def set_state(
PresenceState.BUSY,
)

busy_presence_enabled = await self.hs.get_datastores().main.get_feature_enabled(
target_user.to_string(), "msc3026"
)
if not busy_presence_enabled:
busy_presence_enabled = self.hs.config.experimental.msc3026_enabled

if presence not in valid_presence or (
presence == PresenceState.BUSY and not self._busy_presence_enabled
presence == PresenceState.BUSY and not busy_presence_enabled
):
raise SynapseError(400, "Invalid presence state")

Expand All @@ -1257,7 +1265,7 @@ async def set_state(
new_fields["status_msg"] = status_msg

if presence == PresenceState.ONLINE or (
presence == PresenceState.BUSY and self._busy_presence_enabled
presence == PresenceState.BUSY and busy_presence_enabled
):
new_fields["last_active_ts"] = self.clock.time_msec()

Expand Down
47 changes: 28 additions & 19 deletions synapse/push/bulk_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
Union,
)

import attr
from prometheus_client import Counter

from synapse.api.constants import (
Expand Down Expand Up @@ -107,6 +108,17 @@ def _should_count_as_unread(event: EventBase, context: EventContext) -> bool:
return False


@attr.s(slots=True, auto_attribs=True)
class ActionsForUser:
"""
A class to hold the actions for a given event and whether the event should
increment the unread count.
"""

actions: Collection[Union[Mapping, str]]
count_as_unread: bool


class BulkPushRuleEvaluator:
"""Calculates the outcome of push rules for an event for all users in the
room at once.
Expand Down Expand Up @@ -336,15 +348,8 @@ async def _action_for_event_by_user(
# (historical messages persisted in reverse-chronological order).
return

# Disable counting as unread unless the experimental configuration is
# enabled, as it can cause additional (unwanted) rows to be added to the
# event_push_actions table.
count_as_unread = False
if self.hs.config.experimental.msc2654_enabled:
count_as_unread = _should_count_as_unread(event, context)

rules_by_user = await self._get_rules_for_event(event)
actions_by_user: Dict[str, Collection[Union[Mapping, str]]] = {}
actions_by_user: Dict[str, ActionsForUser] = {}

room_member_count = await self.store.get_number_joined_users_in_room(
event.room_id
Expand Down Expand Up @@ -429,17 +434,22 @@ async def _action_for_event_by_user(
if not isinstance(display_name, str):
display_name = None

if count_as_unread:
# Add an element for the current user if the event needs to be marked as
# unread, so that add_push_actions_to_staging iterates over it.
# If the event shouldn't be marked as unread but should notify the
# current user, it'll be added to the dict later.
actions_by_user[uid] = []

actions = evaluator.run(rules, uid, display_name)
if "notify" in actions:
# Push rules say we should notify the user of this event
actions_by_user[uid] = actions

# check whether unread counts are enabled for this user
unread_enabled = await self.store.get_feature_enabled(uid, "msc2654")
if not unread_enabled:
unread_enabled = self.hs.config.experimental.msc2654_enabled
clokep marked this conversation as resolved.
Show resolved Hide resolved

if unread_enabled:
count_as_unread = _should_count_as_unread(event, context)
else:
count_as_unread = False

if "notify" in actions or count_as_unread:
# Push rules say we should notify the user of this event or the event should
# increment the unread count
actions_by_user[uid] = ActionsForUser(actions, count_as_unread)

# If there aren't any actions then we can skip the rest of the
# processing.
Expand Down Expand Up @@ -467,7 +477,6 @@ async def _action_for_event_by_user(
await self.store.add_push_actions_to_staging(
event.event_id,
actions_by_user,
count_as_unread,
thread_id,
)

Expand Down
8 changes: 7 additions & 1 deletion synapse/rest/client/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,13 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
user_id = requester.user.to_string()
body = parse_json_object_from_request(request)

if self.hs.config.experimental.msc3967_enabled:
msc3967_enabled = await self.hs.get_datastores().main.get_feature_enabled(
user_id, "msc3967"
)
if not msc3967_enabled:
msc3967_enabled = self.hs.config.experimental.msc2654_enabled

if msc3967_enabled:
if await self.e2e_keys_handler.is_cross_signing_set_up_for_user(user_id):
# If we already have a master key then cross signing is set up and we require UIA to reset
await self.auth_handler.validate_user_via_ui_auth(
Expand Down
18 changes: 14 additions & 4 deletions synapse/rest/client/pusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def __init__(self, hs: "HomeServer"):
super().__init__()
self.hs = hs
self.auth = hs.get_auth()
self._msc3881_enabled = self.hs.config.experimental.msc3881_enabled

async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request)
Expand All @@ -54,8 +53,14 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:

pusher_dicts = [p.as_dict() for p in pushers]

msc3881_enabled = await self.hs.get_datastores().main.get_feature_enabled(
user.to_string(), "msc3881"
)
if not msc3881_enabled:
msc3881_enabled = self.hs.config.experimental.msc3881_enabled

for pusher in pusher_dicts:
if self._msc3881_enabled:
if msc3881_enabled:
pusher["org.matrix.msc3881.enabled"] = pusher["enabled"]
pusher["org.matrix.msc3881.device_id"] = pusher["device_id"]
del pusher["enabled"]
Expand All @@ -73,7 +78,6 @@ def __init__(self, hs: "HomeServer"):
self.auth = hs.get_auth()
self.notifier = hs.get_notifier()
self.pusher_pool = self.hs.get_pusherpool()
self._msc3881_enabled = self.hs.config.experimental.msc3881_enabled

async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request)
Expand Down Expand Up @@ -113,7 +117,13 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
append = content["append"]

enabled = True
if self._msc3881_enabled and "org.matrix.msc3881.enabled" in content:
msc3881_enabled = await self.hs.get_datastores().main.get_feature_enabled(
user.to_string(), "msc3881"
)
if not msc3881_enabled:
msc3881_enabled = self.hs.config.experimental.msc3881_enabled

if msc3881_enabled and "org.matrix.msc3881.enabled" in content:
enabled = content["org.matrix.msc3881.enabled"]

if not append:
Expand Down
32 changes: 26 additions & 6 deletions synapse/rest/client/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ def __init__(self, hs: "HomeServer"):
self.presence_handler = hs.get_presence_handler()
self._server_notices_sender = hs.get_server_notices_sender()
self._event_serializer = hs.get_event_client_serializer()
self._msc2654_enabled = hs.config.experimental.msc2654_enabled
self._msc3773_enabled = hs.config.experimental.msc3773_enabled

async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
Expand Down Expand Up @@ -261,7 +260,7 @@ async def encode_response(
)

joined = await self.encode_joined(
sync_result.joined, time_now, serialize_options
sync_result.joined, time_now, serialize_options, requester
)

invited = await self.encode_invited(
Expand All @@ -273,7 +272,7 @@ async def encode_response(
)

archived = await self.encode_archived(
sync_result.archived, time_now, serialize_options
sync_result.archived, time_now, serialize_options, requester
)

logger.debug("building sync response dict")
Expand Down Expand Up @@ -344,6 +343,7 @@ async def encode_joined(
rooms: List[JoinedSyncResult],
time_now: int,
serialize_options: SerializeEventConfig,
requester: Requester,
) -> JsonDict:
"""
Encode the joined rooms in a sync result
Expand All @@ -352,13 +352,18 @@ async def encode_joined(
rooms: list of sync results for rooms this user is joined to
time_now: current time - used as a baseline for age calculations
serialize_options: Event serializer options
requester: The requester of the sync
Returns:
The joined rooms list, in our response format
"""
joined = {}
for room in rooms:
joined[room.room_id] = await self.encode_room(
room, time_now, joined=True, serialize_options=serialize_options
room,
time_now,
joined=True,
serialize_options=serialize_options,
H-Shay marked this conversation as resolved.
Show resolved Hide resolved
requester=requester,
)

return joined
Expand Down Expand Up @@ -449,6 +454,7 @@ async def encode_archived(
rooms: List[ArchivedSyncResult],
time_now: int,
serialize_options: SerializeEventConfig,
requester: Requester,
) -> JsonDict:
"""
Encode the archived rooms in a sync result
Expand All @@ -457,13 +463,18 @@ async def encode_archived(
rooms: list of sync results for rooms this user is joined to
time_now: current time - used as a baseline for age calculations
serialize_options: Event serializer options
requester: the requester of the sync
Returns:
The archived rooms list, in our response format
"""
joined = {}
for room in rooms:
joined[room.room_id] = await self.encode_room(
room, time_now, joined=False, serialize_options=serialize_options
room,
time_now,
joined=False,
serialize_options=serialize_options,
requester=requester,
)

return joined
Expand All @@ -474,6 +485,7 @@ async def encode_room(
time_now: int,
joined: bool,
serialize_options: SerializeEventConfig,
requester: Requester,
) -> JsonDict:
"""
Args:
Expand All @@ -486,6 +498,7 @@ async def encode_room(
only_fields: Optional. The list of event fields to include.
event_formatter: function to convert from federation format
to client format
Requester: The requester of the sync
Returns:
The room, encoded in our response format
"""
Expand Down Expand Up @@ -539,7 +552,14 @@ async def encode_room(
"org.matrix.msc3773.unread_thread_notifications"
] = room.unread_thread_notifications
result["summary"] = room.summary
if self._msc2654_enabled:

msc2654_enabled = await self.hs.get_datastores().main.get_feature_enabled(
requester.user.to_string(), "msc2654"
)
if not msc2654_enabled:
msc2654_enabled = self.hs.config.experimental.msc2654_enabled

if msc2654_enabled:
result["org.matrix.msc2654.unread_count"] = room.unread_count

return result
Expand Down
4 changes: 2 additions & 2 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
"io.element.e2ee_forced.private": self.e2ee_forced_private,
"io.element.e2ee_forced.trusted_private": self.e2ee_forced_trusted_private,
# Supports the busy presence state described in MSC3026.
"org.matrix.msc3026.busy_presence": self.config.experimental.msc3026_enabled,
"org.matrix.msc3026.busy_presence": True,
Comment on lines 98 to +99
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is really accurate -- don't we need to check this based on which user is requesting /versions?

For example, a client might use this to enable UI allowing users to say they're busy. The user would then receive an unexpected error when attempting to use that feature.

(Similar feedback to below for MSC3881)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think you're right - it would be True or False based on either the config or whether the feature is enabled in the DB, but how would that be checked here per user? Is it possible to extract the username from the GET request to /versions (my suspicion is no?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind I figured it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I thought I figured it out but since /versions doesn't require authentication there's no access token to extract the user id from. Is there a way to figure out the user id from a request that doesn't require authentication? If not, what's the best way to handle what /versions should return here if we can't determine the user (and thus cannot check if it is enabled per user)?

Copy link
Contributor

@reivilibre reivilibre May 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing I can think of is to make it optionally accept authentication and ask the clients to pretty please change their request code so it sends auth for /versions if they want to use unstable features... :/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I wonder if we should start putting unstable features in /capabilities API (which is authed) instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I wonder if we should start putting unstable features in /capabilities API (which is authed) instead?

I think at some point I was told to prefer /versions over /capabilities, but after some searching I can't seem to find a reference to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't seem to find a reference to this.

It's in the spec: https://spec.matrix.org/v1.6/client-server-api/#capabilities-negotiation
The relevant bit is:

This system should not be used to advertise unstable or experimental features - this is better done by the /versions endpoint.

Given this it seems like Oliver's suggestion is the way to go? Any other suggestions I should consider?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this it seems like Oliver's suggestion is the way to go? Any other suggestions I should consider?

I'd probably double check if clients would be willing to do this first?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSC4026 was written for this, it has now passed FCP.

# Supports receiving private read receipts as per MSC2285
"org.matrix.msc2285.stable": True, # TODO: Remove when MSC2285 becomes a part of the spec
# Supports filtering of /publicRooms by room type as per MSC3827
Expand All @@ -115,7 +115,7 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
# Adds support for login token requests as per MSC3882
"org.matrix.msc3882": self.config.experimental.msc3882_enabled,
# Adds support for remotely enabling/disabling pushers, as per MSC3881
"org.matrix.msc3881": self.config.experimental.msc3881_enabled,
"org.matrix.msc3881": True,
# Adds support for filtering /messages by event relation.
"org.matrix.msc3874": self.config.experimental.msc3874_enabled,
# Adds support for simple HTTP rendezvous as per MSC3886
Expand Down
Loading