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

Allow selecting "prejoin" events by state keys #14642

Merged
merged 15 commits into from
Dec 13, 2022
1 change: 1 addition & 0 deletions changelog.d/14642.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow selecting "prejoin" events by state keys in addition to event types.
57 changes: 39 additions & 18 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2501,32 +2501,53 @@ Config settings related to the client/server API
---
### `room_prejoin_state`

Controls for the state that is shared with users who receive an invite
to a room. By default, the following state event types are shared with users who
receive invites to the room:
- m.room.join_rules
- m.room.canonical_alias
- m.room.avatar
- m.room.encryption
- m.room.name
- m.room.create
- m.room.topic
This setting controls the state that is shared with users upon receiving an
invite to a room, or in reply to a knock on a room. By default, the following
state events are shared with users:

- `m.room.join_rules`
- `m.room.canonical_alias`
- `m.room.avatar`
- `m.room.encryption`
- `m.room.name`
- `m.room.create`
- `m.room.topic`

To change the default behavior, use the following sub-options:
* `disable_default_event_types`: set to true to disable the above defaults. If this
is enabled, only the event types listed in `additional_event_types` are shared.
Defaults to false.
* `additional_event_types`: Additional state event types to share with users when they are invited
to a room. By default, this list is empty (so only the default event types are shared).
* `disable_default_event_types`: boolean. Set to `true` to disable the above
defaults. If this is enabled, only the event types listed in
`additional_event_types` are shared. Defaults to `false`.
* `additional_event_types`: A list of additional state events to include in the
events to be shared. By default, this list is empty (so only the default event
types are shared).

Each entry in this list should be either a single string or a list of two
strings.
* A standalone string `t` represents all events with type `t` (i.e.
with no restrictions on state keys).
* A pair of strings `[t, s]` represents a single event with type `t` and
state key `s`. The same type can appear in two entries with different state
keys: in this situation, both state keys are included in prejoin state.

Example configuration:
```yaml
room_prejoin_state:
disable_default_event_types: true
disable_default_event_types: false
additional_event_types:
- org.example.custom.event.type
- m.room.join_rules
# Share all events of type `org.example.custom.event.typeA`
- org.example.custom.event.typeA
clokep marked this conversation as resolved.
Show resolved Hide resolved
# Share only events of type `org.example.custom.event.typeB` whose
# state_key is "foo"
- ["org.example.custom.event.typeB", "foo"]
# Share only events of type `org.example.custom.event.typeC` whose
# state_key is "bar" or "baz"
- ["org.example.custom.event.typeC", "bar"]
- ["org.example.custom.event.typeC", "baz"]
```

*Changed in Synapse 1.74:* admins can filter the events in prejoin state based
on their state key.

---
### `track_puppeted_user_ips`

Expand Down
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ disallow_untyped_defs = False
[mypy-tests.*]
disallow_untyped_defs = False

[mypy-tests.config.test_api]
disallow_untyped_defs = True

[mypy-tests.handlers.test_sso]
disallow_untyped_defs = True

Expand Down
3 changes: 3 additions & 0 deletions synapse/config/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def validate_config(
config: the configuration value to be validated
config_path: the path within the config file. This will be used as a basis
for the error message.

Raises:
ConfigError, if validation fails.
"""
try:
jsonschema.validate(config, json_schema)
Expand Down
104 changes: 83 additions & 21 deletions synapse/config/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# limitations under the License.

import logging
from typing import Any, Iterable
from typing import Any, Container, Dict, Iterable, Mapping, Optional, Set, Tuple, Type

import attr

from synapse.api.constants import EventTypes
from synapse.config._base import Config, ConfigError
Expand All @@ -23,19 +25,63 @@
logger = logging.getLogger(__name__)


@attr.s(auto_attribs=True)
class StateKeyFilter(Container[str]):
"""A simpler version of StateFilter which ignores event types.

Represents an optional constraint that state_keys must belong to a given set of
strings called `options`. An empty set of `options` means that there are no
restrictions.
"""

options: Set[str]

@classmethod
def any(cls: Type["StateKeyFilter"]) -> "StateKeyFilter":
return cls(set())

@classmethod
def only(cls: Type["StateKeyFilter"], state_key: str) -> "StateKeyFilter":
return cls({state_key})

def __contains__(self, state_key: object) -> bool:
return not self.options or state_key in self.options

def add(self, state_key: Optional[str]) -> None:
if state_key is None:
self.options = set()
elif self.options:
self.options.add(state_key)


class ApiConfig(Config):
section = "api"

room_prejoin_state: Mapping[str, StateKeyFilter]
track_puppetted_users_ips: bool

def read_config(self, config: JsonDict, **kwargs: Any) -> None:
validate_config(_MAIN_SCHEMA, config, ())
self.room_prejoin_state = list(self._get_prejoin_state_types(config))
self.room_prejoin_state = self._build_prejoin_state(config)
self.track_puppeted_user_ips = config.get("track_puppeted_user_ips", False)

def _get_prejoin_state_types(self, config: JsonDict) -> Iterable[str]:
"""Get the event types to include in the prejoin state

Parses the config and returns an iterable of the event types to be included.
"""
def _build_prejoin_state(self, config: JsonDict) -> Dict[str, StateKeyFilter]:
prejoin_events = {}
for event_type, state_key in self._get_prejoin_state_entries(config):
if event_type not in prejoin_events:
if state_key is None:
filter = StateKeyFilter.any()
else:
filter = StateKeyFilter.only(state_key)
prejoin_events[event_type] = filter
else:
prejoin_events[event_type].add(state_key)
Copy link
Contributor

Choose a reason for hiding this comment

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

can state_key be None here? do we need to guard against that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

can state_key be None here?

Yes, if someone specifies an entry consisting only of an event type.

do we need to guard against that?

No, it's a valid input that's explicitly handled by .add(None), see

def add(self, state_key: Optional[str]) -> None:
if state_key is None:
self.options = set()

return prejoin_events
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This wasn't particularly neat, but it gets the job done.


def _get_prejoin_state_entries(
self, config: JsonDict
) -> Iterable[Tuple[str, Optional[str]]]:
"""Get the event types and state keys to include in the prejoin state."""
room_prejoin_state_config = config.get("room_prejoin_state") or {}

# backwards-compatibility support for room_invite_state_types
Expand All @@ -50,33 +96,39 @@ def _get_prejoin_state_types(self, config: JsonDict) -> Iterable[str]:

logger.warning(_ROOM_INVITE_STATE_TYPES_WARNING)

yield from config["room_invite_state_types"]
for event_type in config["room_invite_state_types"]:
yield event_type, None
return

if not room_prejoin_state_config.get("disable_default_event_types"):
yield from _DEFAULT_PREJOIN_STATE_TYPES
yield from _DEFAULT_PREJOIN_STATE_TYPES_AND_STATE_KEYS

yield from room_prejoin_state_config.get("additional_event_types", [])
for entry in room_prejoin_state_config.get("additional_event_types", []):
if isinstance(entry, str):
yield entry, None
else:
yield entry


_ROOM_INVITE_STATE_TYPES_WARNING = """\
WARNING: The 'room_invite_state_types' configuration setting is now deprecated,
and replaced with 'room_prejoin_state'. New features may not work correctly
unless 'room_invite_state_types' is removed. See the sample configuration file for
details of 'room_prejoin_state'.
unless 'room_invite_state_types' is removed. See the config documentation at
https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html#room_prejoin_state
for details of 'room_prejoin_state'.
--------------------------------------------------------------------------------
"""

_DEFAULT_PREJOIN_STATE_TYPES = [
EventTypes.JoinRules,
EventTypes.CanonicalAlias,
EventTypes.RoomAvatar,
EventTypes.RoomEncryption,
EventTypes.Name,
_DEFAULT_PREJOIN_STATE_TYPES_AND_STATE_KEYS = [
(EventTypes.JoinRules, ""),
(EventTypes.CanonicalAlias, ""),
(EventTypes.RoomAvatar, ""),
(EventTypes.RoomEncryption, ""),
(EventTypes.Name, ""),
# Per MSC1772.
EventTypes.Create,
(EventTypes.Create, ""),
# Per MSC3173.
EventTypes.Topic,
(EventTypes.Topic, ""),
]


Expand All @@ -90,7 +142,17 @@ def _get_prejoin_state_types(self, config: JsonDict) -> Iterable[str]:
"disable_default_event_types": {"type": "boolean"},
"additional_event_types": {
"type": "array",
"items": {"type": "string"},
"items": {
"oneOf": [
{"type": "string"},
{
"type": "array",
"items": {"type": "string"},
"minItems": 2,
"maxItems": 2,
},
],
},
},
},
},
Expand Down
32 changes: 31 additions & 1 deletion synapse/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@
)

import attr
from canonicaljson import encode_canonical_json

from synapse.api.constants import EventContentFields, EventTypes, RelationTypes
from synapse.api.constants import (
MAX_PDU_SIZE,
EventContentFields,
EventTypes,
RelationTypes,
)
from synapse.api.errors import Codes, SynapseError
from synapse.api.room_versions import RoomVersion
from synapse.types import JsonDict
Expand Down Expand Up @@ -674,3 +680,27 @@ def validate_canonicaljson(value: Any) -> None:
elif not isinstance(value, (bool, str)) and value is not None:
# Other potential JSON values (bool, None, str) are safe.
raise SynapseError(400, "Unknown JSON value", Codes.BAD_JSON)


def maybe_upsert_event_field(
event: EventBase, container: JsonDict, key: str, value: object
) -> bool:
"""Upsert an event field, but only if this doesn't make the event too large.

Returns true iff the upsert took place.
"""
if key in container:
old_value: object = container[key]
container[key] = value
# NB: here and below, we assume that passing a non-None `time_now` argument to
# get_pdu_json doesn't increase the size of the encoded result.
upsert_okay = len(encode_canonical_json(event.get_pdu_json())) <= MAX_PDU_SIZE
if not upsert_okay:
container[key] = old_value
else:
container[key] = value
upsert_okay = len(encode_canonical_json(event.get_pdu_json())) <= MAX_PDU_SIZE
if not upsert_okay:
del container[key]

return upsert_okay
29 changes: 18 additions & 11 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from synapse.events import EventBase, relation_from_event
from synapse.events.builder import EventBuilder
from synapse.events.snapshot import EventContext
from synapse.events.utils import maybe_upsert_event_field
from synapse.events.validator import EventValidator
from synapse.handlers.directory import DirectoryHandler
from synapse.logging import opentracing
Expand Down Expand Up @@ -1739,12 +1740,15 @@ async def persist_and_notify_client_events(

if event.type == EventTypes.Member:
if event.content["membership"] == Membership.INVITE:
event.unsigned[
"invite_room_state"
] = await self.store.get_stripped_room_state_from_event_context(
context,
self.room_prejoin_state_types,
membership_user_id=event.sender,
maybe_upsert_event_field(
event,
event.unsigned,
"invite_room_state",
await self.store.get_stripped_room_state_from_event_context(
context,
self.room_prejoin_state_types,
membership_user_id=event.sender,
),
)

invitee = UserID.from_string(event.state_key)
Expand All @@ -1762,11 +1766,14 @@ async def persist_and_notify_client_events(
event.signatures.update(returned_invite.signatures)

if event.content["membership"] == Membership.KNOCK:
event.unsigned[
"knock_room_state"
] = await self.store.get_stripped_room_state_from_event_context(
context,
self.room_prejoin_state_types,
maybe_upsert_event_field(
event,
event.unsigned,
"knock_room_state",
await self.store.get_stripped_room_state_from_event_context(
context,
self.room_prejoin_state_types,
),
)

if event.type == EventTypes.Redaction:
Expand Down
Loading