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

Add new module API for adding custom fields to events unsigned section #16549

Merged
merged 8 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog.d/16549.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a new module API callback that allows adding extra fields to events' unsigned section when sent down to clients.
3 changes: 2 additions & 1 deletion docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# Usage
- [Federation](federate.md)
- [Configuration](usage/configuration/README.md)
- [Configuration Manual](usage/configuration/config_documentation.md)
- [Configuration Manual](usage/configuration/config_documentation.md)
- [Homeserver Sample Config File](usage/configuration/homeserver_sample_config.md)
- [Logging Sample Config File](usage/configuration/logging_sample_config.md)
- [Structured Logging](structured_logging.md)
Expand Down Expand Up @@ -48,6 +48,7 @@
- [Password auth provider callbacks](modules/password_auth_provider_callbacks.md)
- [Background update controller callbacks](modules/background_update_controller_callbacks.md)
- [Account data callbacks](modules/account_data_callbacks.md)
- [Add extra fields to client events unsigned section callbacks](modules/add_extra_fields_to_client_events_unsigned.md)
- [Porting a legacy module to the new interface](modules/porting_legacy_module.md)
- [Workers](workers.md)
- [Using `synctl` with Workers](synctl_workers.md)
Expand Down
32 changes: 32 additions & 0 deletions docs/modules/add_extra_fields_to_client_events_unsigned.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Add extra fields to client events unsigned section callbacks

_First introduced in Synapse v1.96.0_

This callback allows modules to add extra fields to the unsigned section of
events when they get sent down to clients.

These get called *every* time an event is to be sent to clients, so care should
be taken to ensure with respect to performance.

### API
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved

To register the callback, use
`register_add_extra_fields_to_unsigned_client_event_callbacks` on the
`ModuleApi`.

The callback should be of the form

```python
async def add_field_to_unsigned(
event: EventBase,
) -> JsonDict:
```
Comment on lines +19 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts:

  • the module doesn't know who the event is destined for. Would it be useful to include a receipient mxid/device here? (I'm thinking of e.g. txn ids, which we only send down to the device that requested it). I expect the answer is "we don't need it now, but we can add it in the future if it's necessary".
  • Could the callback return different things in different invocations for the same event? If so---do we care/need to worry?

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. I think it could be useful in the future, but it's not needed for now and I don't really want to have to do the refactoring to support it right now. As you say, its easy to add later.
  2. Yes, the callback can give different things for the same event, but that doesn't matter


where the extra fields to add to the event's unsigned section is returned.
(Modules must not attempt to modify the `event` directly).

This cannot be used to alter the "core" fields in the unsigned section emitted
by Synapse itself.

If multiple such callbacks try to add the same field to an event's unsigned
section, the last-registered callback wins.
48 changes: 41 additions & 7 deletions synapse/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
Iterable,
Expand Down Expand Up @@ -45,6 +46,7 @@

if TYPE_CHECKING:
from synapse.handlers.relations import BundledAggregations
from synapse.server import HomeServer


# Split strings on "." but not "\." (or "\\\.").
Expand All @@ -56,6 +58,13 @@
CANONICALJSON_MIN_INT = -CANONICALJSON_MAX_INT


# Module API callback that allows adding fields to the unsigned section of
# events that are sent to clients.
ADD_EXTRA_FIELDS_TO_UNSIGNED_CLIENT_EVENT_CALLBACK = Callable[
[EventBase], Awaitable[JsonDict]
]


def prune_event(event: EventBase) -> EventBase:
"""Returns a pruned version of the given event, which removes all keys we
don't know about or think could potentially be dodgy.
Expand Down Expand Up @@ -509,7 +518,13 @@ class EventClientSerializer:
clients.
"""

def serialize_event(
def __init__(self, hs: "HomeServer") -> None:
self._store = hs.get_datastores().main
self._add_extra_fields_to_unsigned_client_event_callbacks: List[
ADD_EXTRA_FIELDS_TO_UNSIGNED_CLIENT_EVENT_CALLBACK
] = []

async def serialize_event(
self,
event: Union[JsonDict, EventBase],
time_now: int,
Expand All @@ -535,10 +550,21 @@ def serialize_event(

serialized_event = serialize_event(event, time_now, config=config)

new_unsigned = {}
for callback in self._add_extra_fields_to_unsigned_client_event_callbacks:
u = await callback(event)
new_unsigned.update(u)

if new_unsigned:
# We do the `update` this way round so that modules can't clobber
# existing fields.
new_unsigned.update(serialized_event["unsigned"])
serialized_event["unsigned"] = new_unsigned

# Check if there are any bundled aggregations to include with the event.
if bundle_aggregations:
if event.event_id in bundle_aggregations:
self._inject_bundled_aggregations(
await self._inject_bundled_aggregations(
event,
time_now,
config,
Expand All @@ -548,7 +574,7 @@ def serialize_event(

return serialized_event

def _inject_bundled_aggregations(
async def _inject_bundled_aggregations(
self,
event: EventBase,
time_now: int,
Expand Down Expand Up @@ -590,7 +616,7 @@ def _inject_bundled_aggregations(
# said that we should only include the `event_id`, `origin_server_ts` and
# `sender` of the edit; however MSC3925 proposes extending it to the whole
# of the edit, which is what we do here.
serialized_aggregations[RelationTypes.REPLACE] = self.serialize_event(
serialized_aggregations[RelationTypes.REPLACE] = await self.serialize_event(
event_aggregations.replace,
time_now,
config=config,
Expand All @@ -600,7 +626,7 @@ def _inject_bundled_aggregations(
if event_aggregations.thread:
thread = event_aggregations.thread

serialized_latest_event = self.serialize_event(
serialized_latest_event = await self.serialize_event(
thread.latest_event,
time_now,
config=config,
Expand All @@ -623,7 +649,7 @@ def _inject_bundled_aggregations(
"m.relations", {}
).update(serialized_aggregations)

def serialize_events(
async def serialize_events(
self,
events: Iterable[Union[JsonDict, EventBase]],
time_now: int,
Expand All @@ -645,7 +671,7 @@ def serialize_events(
The list of serialized events
"""
return [
self.serialize_event(
await self.serialize_event(
event,
time_now,
config=config,
Expand All @@ -654,6 +680,14 @@ def serialize_events(
for event in events
]

def register_add_extra_fields_to_unsigned_client_event_callback(
self, callback: ADD_EXTRA_FIELDS_TO_UNSIGNED_CLIENT_EVENT_CALLBACK
) -> None:
"""Register a callback that returns additions to the unsigned section of
serialized events.
"""
self._add_extra_fields_to_unsigned_client_event_callbacks.append(callback)


_PowerLevel = Union[str, int]
PowerLevelsContent = Mapping[str, Union[_PowerLevel, Mapping[str, _PowerLevel]]]
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async def get_stream(

events.extend(to_add)

chunks = self._event_serializer.serialize_events(
chunks = await self._event_serializer.serialize_events(
events,
time_now,
config=SerializeEventConfig(
Expand Down
14 changes: 7 additions & 7 deletions synapse/handlers/initial_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ async def handle_room(event: RoomsForUser) -> None:
d["inviter"] = event.sender

invite_event = await self.store.get_event(event.event_id)
d["invite"] = self._event_serializer.serialize_event(
d["invite"] = await self._event_serializer.serialize_event(
invite_event,
time_now,
config=serializer_options,
Expand Down Expand Up @@ -225,7 +225,7 @@ async def handle_room(event: RoomsForUser) -> None:

d["messages"] = {
"chunk": (
self._event_serializer.serialize_events(
await self._event_serializer.serialize_events(
messages,
time_now=time_now,
config=serializer_options,
Expand All @@ -235,7 +235,7 @@ async def handle_room(event: RoomsForUser) -> None:
"end": await end_token.to_string(self.store),
}

d["state"] = self._event_serializer.serialize_events(
d["state"] = await self._event_serializer.serialize_events(
current_state.values(),
time_now=time_now,
config=serializer_options,
Expand Down Expand Up @@ -387,7 +387,7 @@ async def _room_initial_sync_parted(
"messages": {
"chunk": (
# Don't bundle aggregations as this is a deprecated API.
self._event_serializer.serialize_events(
await self._event_serializer.serialize_events(
messages, time_now, config=serialize_options
)
),
Expand All @@ -396,7 +396,7 @@ async def _room_initial_sync_parted(
},
"state": (
# Don't bundle aggregations as this is a deprecated API.
self._event_serializer.serialize_events(
await self._event_serializer.serialize_events(
room_state.values(), time_now, config=serialize_options
)
),
Expand All @@ -420,7 +420,7 @@ async def _room_initial_sync_joined(
time_now = self.clock.time_msec()
serialize_options = SerializeEventConfig(requester=requester)
# Don't bundle aggregations as this is a deprecated API.
state = self._event_serializer.serialize_events(
state = await self._event_serializer.serialize_events(
current_state.values(),
time_now,
config=serialize_options,
Expand Down Expand Up @@ -497,7 +497,7 @@ async def get_receipts() -> List[JsonMapping]:
"messages": {
"chunk": (
# Don't bundle aggregations as this is a deprecated API.
self._event_serializer.serialize_events(
await self._event_serializer.serialize_events(
messages, time_now, config=serialize_options
)
),
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ async def get_state_events(
)
room_state = room_state_events[membership_event_id]

events = self._event_serializer.serialize_events(
events = await self._event_serializer.serialize_events(
room_state.values(),
self.clock.time_msec(),
config=SerializeEventConfig(requester=requester),
Expand Down
4 changes: 2 additions & 2 deletions synapse/handlers/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ async def get_messages(

chunk = {
"chunk": (
self._event_serializer.serialize_events(
await self._event_serializer.serialize_events(
events,
time_now,
config=serialize_options,
Expand All @@ -669,7 +669,7 @@ async def get_messages(
}

if state:
chunk["state"] = self._event_serializer.serialize_events(
chunk["state"] = await self._event_serializer.serialize_events(
state, time_now, config=serialize_options
)

Expand Down
8 changes: 5 additions & 3 deletions synapse/handlers/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ async def get_relations(
now = self._clock.time_msec()
serialize_options = SerializeEventConfig(requester=requester)
return_value: JsonDict = {
"chunk": self._event_serializer.serialize_events(
"chunk": await self._event_serializer.serialize_events(
events,
now,
bundle_aggregations=aggregations,
Expand All @@ -177,7 +177,9 @@ async def get_relations(
if include_original_event:
# Do not bundle aggregations when retrieving the original event because
# we want the content before relations are applied to it.
return_value["original_event"] = self._event_serializer.serialize_event(
return_value[
"original_event"
] = await self._event_serializer.serialize_event(
event,
now,
bundle_aggregations=None,
Expand Down Expand Up @@ -602,7 +604,7 @@ async def get_threads(
)

now = self._clock.time_msec()
serialized_events = self._event_serializer.serialize_events(
serialized_events = await self._event_serializer.serialize_events(
events, now, bundle_aggregations=aggregations
)

Expand Down
8 changes: 4 additions & 4 deletions synapse/handlers/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,13 @@ async def _search(
serialize_options = SerializeEventConfig(requester=requester)

for context in contexts.values():
context["events_before"] = self._event_serializer.serialize_events(
context["events_before"] = await self._event_serializer.serialize_events(
context["events_before"],
time_now,
bundle_aggregations=aggregations,
config=serialize_options,
)
context["events_after"] = self._event_serializer.serialize_events(
context["events_after"] = await self._event_serializer.serialize_events(
context["events_after"],
time_now,
bundle_aggregations=aggregations,
Expand All @@ -390,7 +390,7 @@ async def _search(
results = [
{
"rank": search_result.rank_map[e.event_id],
"result": self._event_serializer.serialize_event(
"result": await self._event_serializer.serialize_event(
e,
time_now,
bundle_aggregations=aggregations,
Expand All @@ -409,7 +409,7 @@ async def _search(

if state_results:
rooms_cat_res["state"] = {
room_id: self._event_serializer.serialize_events(
room_id: await self._event_serializer.serialize_events(
state_events, time_now, config=serialize_options
)
for room_id, state_events in state_results.items()
Expand Down
21 changes: 21 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
GET_USERS_FOR_STATES_CALLBACK,
PresenceRouter,
)
from synapse.events.utils import ADD_EXTRA_FIELDS_TO_UNSIGNED_CLIENT_EVENT_CALLBACK
from synapse.handlers.account_data import ON_ACCOUNT_DATA_UPDATED_CALLBACK
from synapse.handlers.auth import (
CHECK_3PID_AUTH_CALLBACK,
Expand Down Expand Up @@ -257,6 +258,7 @@ def __init__(self, hs: "HomeServer", auth_handler: AuthHandler) -> None:
self.custom_template_dir = hs.config.server.custom_template_directory
self._callbacks = hs.get_module_api_callbacks()
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
self._event_serializer = hs.get_event_client_serializer()

try:
app_name = self._hs.config.email.email_app_name
Expand Down Expand Up @@ -488,6 +490,25 @@ def register_web_resource(self, path: str, resource: Resource) -> None:
"""
self._hs.register_module_web_resource(path, resource)

def register_add_extra_fields_to_unsigned_client_event_callbacks(
self,
*,
add_field_to_unsigned_callback: Optional[
ADD_EXTRA_FIELDS_TO_UNSIGNED_CLIENT_EVENT_CALLBACK
] = None,
) -> None:
"""Registers a callback that can be used to add fields to the unsigned
section of events.

The callback is called every time an event is sent down to a client.

Added in Synapse 1.96.0
"""
if add_field_to_unsigned_callback is not None:
self._event_serializer.register_add_extra_fields_to_unsigned_client_event_callback(
add_field_to_unsigned_callback
)

#########################################################################
# The following methods can be called by the module at any point in time.

Expand Down
Loading
Loading