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

Commit 86a515c

Browse files
authored
Consolidate logic for parsing relations. (#12693)
Parse the `m.relates_to` event content field (which describes relations) in a single place, this is used during: * Event persistence. * Validation of the Client-Server API. * Fetching bundled aggregations. * Processing of push rules. Each of these separately implement the logic and each made slightly different assumptions about what was valid. Some had minor / potential bugs.
1 parent cde8af9 commit 86a515c

File tree

7 files changed

+98
-61
lines changed

7 files changed

+98
-61
lines changed

changelog.d/12693.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Consolidate parsing of relation information from events.

synapse/events/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# limitations under the License.
1616

1717
import abc
18+
import collections.abc
1819
import os
1920
from typing import (
2021
TYPE_CHECKING,
@@ -32,9 +33,11 @@
3233
overload,
3334
)
3435

36+
import attr
3537
from typing_extensions import Literal
3638
from unpaddedbase64 import encode_base64
3739

40+
from synapse.api.constants import RelationTypes
3841
from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVersions
3942
from synapse.types import JsonDict, RoomStreamToken
4043
from synapse.util.caches import intern_dict
@@ -615,3 +618,45 @@ def make_event_from_dict(
615618
return event_type(
616619
event_dict, room_version, internal_metadata_dict or {}, rejected_reason
617620
)
621+
622+
623+
@attr.s(slots=True, frozen=True, auto_attribs=True)
624+
class _EventRelation:
625+
# The target event of the relation.
626+
parent_id: str
627+
# The relation type.
628+
rel_type: str
629+
# The aggregation key. Will be None if the rel_type is not m.annotation or is
630+
# not a string.
631+
aggregation_key: Optional[str]
632+
633+
634+
def relation_from_event(event: EventBase) -> Optional[_EventRelation]:
635+
"""
636+
Attempt to parse relation information an event.
637+
638+
Returns:
639+
The event relation information, if it is valid. None, otherwise.
640+
"""
641+
relation = event.content.get("m.relates_to")
642+
if not relation or not isinstance(relation, collections.abc.Mapping):
643+
# No relation information.
644+
return None
645+
646+
# Relations must have a type and parent event ID.
647+
rel_type = relation.get("rel_type")
648+
if not isinstance(rel_type, str):
649+
return None
650+
651+
parent_id = relation.get("event_id")
652+
if not isinstance(parent_id, str):
653+
return None
654+
655+
# Annotations have a key field.
656+
aggregation_key = None
657+
if rel_type == RelationTypes.ANNOTATION:
658+
aggregation_key = relation.get("key")
659+
if not isinstance(aggregation_key, str):
660+
aggregation_key = None
661+
662+
return _EventRelation(parent_id, rel_type, aggregation_key)

synapse/handlers/message.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
4545
from synapse.api.urls import ConsentURIBuilder
4646
from synapse.event_auth import validate_event_for_room_version
47-
from synapse.events import EventBase
47+
from synapse.events import EventBase, relation_from_event
4848
from synapse.events.builder import EventBuilder
4949
from synapse.events.snapshot import EventContext
5050
from synapse.events.validator import EventValidator
@@ -1060,20 +1060,11 @@ async def _validate_event_relation(self, event: EventBase) -> None:
10601060
SynapseError if the event is invalid.
10611061
"""
10621062

1063-
relation = event.content.get("m.relates_to")
1063+
relation = relation_from_event(event)
10641064
if not relation:
10651065
return
10661066

1067-
relation_type = relation.get("rel_type")
1068-
if not relation_type:
1069-
return
1070-
1071-
# Ensure the parent is real.
1072-
relates_to = relation.get("event_id")
1073-
if not relates_to:
1074-
return
1075-
1076-
parent_event = await self.store.get_event(relates_to, allow_none=True)
1067+
parent_event = await self.store.get_event(relation.parent_id, allow_none=True)
10771068
if parent_event:
10781069
# And in the same room.
10791070
if parent_event.room_id != event.room_id:
@@ -1082,28 +1073,31 @@ async def _validate_event_relation(self, event: EventBase) -> None:
10821073
else:
10831074
# There must be some reason that the client knows the event exists,
10841075
# see if there are existing relations. If so, assume everything is fine.
1085-
if not await self.store.event_is_target_of_relation(relates_to):
1076+
if not await self.store.event_is_target_of_relation(relation.parent_id):
10861077
# Otherwise, the client can't know about the parent event!
10871078
raise SynapseError(400, "Can't send relation to unknown event")
10881079

10891080
# If this event is an annotation then we check that that the sender
10901081
# can't annotate the same way twice (e.g. stops users from liking an
10911082
# event multiple times).
1092-
if relation_type == RelationTypes.ANNOTATION:
1093-
aggregation_key = relation["key"]
1083+
if relation.rel_type == RelationTypes.ANNOTATION:
1084+
aggregation_key = relation.aggregation_key
1085+
1086+
if aggregation_key is None:
1087+
raise SynapseError(400, "Missing aggregation key")
10941088

10951089
if len(aggregation_key) > 500:
10961090
raise SynapseError(400, "Aggregation key is too long")
10971091

10981092
already_exists = await self.store.has_user_annotated_event(
1099-
relates_to, event.type, aggregation_key, event.sender
1093+
relation.parent_id, event.type, aggregation_key, event.sender
11001094
)
11011095
if already_exists:
11021096
raise SynapseError(400, "Can't send same reaction twice")
11031097

11041098
# Don't attempt to start a thread if the parent event is a relation.
1105-
elif relation_type == RelationTypes.THREAD:
1106-
if await self.store.event_includes_relation(relates_to):
1099+
elif relation.rel_type == RelationTypes.THREAD:
1100+
if await self.store.event_includes_relation(relation.parent_id):
11071101
raise SynapseError(
11081102
400, "Cannot start threads from an event with a relation"
11091103
)

synapse/handlers/relations.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
import collections.abc
1514
import logging
1615
from typing import (
1716
TYPE_CHECKING,
@@ -28,7 +27,7 @@
2827

2928
from synapse.api.constants import RelationTypes
3029
from synapse.api.errors import SynapseError
31-
from synapse.events import EventBase
30+
from synapse.events import EventBase, relation_from_event
3231
from synapse.storage.databases.main.relations import _RelatedEvent
3332
from synapse.types import JsonDict, Requester, StreamToken, UserID
3433
from synapse.visibility import filter_events_for_client
@@ -373,20 +372,21 @@ async def get_bundled_aggregations(
373372
if event.is_state():
374373
continue
375374

376-
relates_to = event.content.get("m.relates_to")
377-
relation_type = None
378-
if isinstance(relates_to, collections.abc.Mapping):
379-
relation_type = relates_to.get("rel_type")
375+
relates_to = relation_from_event(event)
376+
if relates_to:
380377
# An event which is a replacement (ie edit) or annotation (ie,
381378
# reaction) may not have any other event related to it.
382-
if relation_type in (RelationTypes.ANNOTATION, RelationTypes.REPLACE):
379+
if relates_to.rel_type in (
380+
RelationTypes.ANNOTATION,
381+
RelationTypes.REPLACE,
382+
):
383383
continue
384384

385+
# Track the event's relation information for later.
386+
relations_by_id[event.event_id] = relates_to.rel_type
387+
385388
# The event should get bundled aggregations.
386389
events_by_id[event.event_id] = event
387-
# Track the event's relation information for later.
388-
if isinstance(relation_type, str):
389-
relations_by_id[event.event_id] = relation_type
390390

391391
# event ID -> bundled aggregation in non-serialized form.
392392
results: Dict[str, BundledAggregations] = {}

synapse/push/bulk_push_rule_evaluator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from synapse.api.constants import EventTypes, Membership, RelationTypes
2323
from synapse.event_auth import get_user_power_level
24-
from synapse.events import EventBase
24+
from synapse.events import EventBase, relation_from_event
2525
from synapse.events.snapshot import EventContext
2626
from synapse.state import POWER_KEY
2727
from synapse.storage.databases.main.roommember import EventIdMembership
@@ -78,8 +78,8 @@ def _should_count_as_unread(event: EventBase, context: EventContext) -> bool:
7878
return False
7979

8080
# Exclude edits.
81-
relates_to = event.content.get("m.relates_to", {})
82-
if relates_to.get("rel_type") == RelationTypes.REPLACE:
81+
relates_to = relation_from_event(event)
82+
if relates_to and relates_to.rel_type == RelationTypes.REPLACE:
8383
return False
8484

8585
# Mark events that have a non-empty string body as unread.

synapse/storage/databases/main/events.py

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
import synapse.metrics
3737
from synapse.api.constants import EventContentFields, EventTypes, RelationTypes
3838
from synapse.api.room_versions import RoomVersions
39-
from synapse.events import EventBase # noqa: F401
40-
from synapse.events.snapshot import EventContext # noqa: F401
39+
from synapse.events import EventBase, relation_from_event
40+
from synapse.events.snapshot import EventContext
4141
from synapse.storage._base import db_to_json, make_in_list_sql_clause
4242
from synapse.storage.database import (
4343
DatabasePool,
@@ -1807,52 +1807,45 @@ def _handle_event_relations(
18071807
txn: The current database transaction.
18081808
event: The event which might have relations.
18091809
"""
1810-
relation = event.content.get("m.relates_to")
1810+
relation = relation_from_event(event)
18111811
if not relation:
1812-
# No relations
1812+
# No relation, nothing to do.
18131813
return
18141814

1815-
# Relations must have a type and parent event ID.
1816-
rel_type = relation.get("rel_type")
1817-
if not isinstance(rel_type, str):
1818-
return
1819-
1820-
parent_id = relation.get("event_id")
1821-
if not isinstance(parent_id, str):
1822-
return
1823-
1824-
# Annotations have a key field.
1825-
aggregation_key = None
1826-
if rel_type == RelationTypes.ANNOTATION:
1827-
aggregation_key = relation.get("key")
1828-
18291815
self.db_pool.simple_insert_txn(
18301816
txn,
18311817
table="event_relations",
18321818
values={
18331819
"event_id": event.event_id,
1834-
"relates_to_id": parent_id,
1835-
"relation_type": rel_type,
1836-
"aggregation_key": aggregation_key,
1820+
"relates_to_id": relation.parent_id,
1821+
"relation_type": relation.rel_type,
1822+
"aggregation_key": relation.aggregation_key,
18371823
},
18381824
)
18391825

1840-
txn.call_after(self.store.get_relations_for_event.invalidate, (parent_id,))
18411826
txn.call_after(
1842-
self.store.get_aggregation_groups_for_event.invalidate, (parent_id,)
1827+
self.store.get_relations_for_event.invalidate, (relation.parent_id,)
1828+
)
1829+
txn.call_after(
1830+
self.store.get_aggregation_groups_for_event.invalidate,
1831+
(relation.parent_id,),
18431832
)
18441833

1845-
if rel_type == RelationTypes.REPLACE:
1846-
txn.call_after(self.store.get_applicable_edit.invalidate, (parent_id,))
1834+
if relation.rel_type == RelationTypes.REPLACE:
1835+
txn.call_after(
1836+
self.store.get_applicable_edit.invalidate, (relation.parent_id,)
1837+
)
18471838

1848-
if rel_type == RelationTypes.THREAD:
1849-
txn.call_after(self.store.get_thread_summary.invalidate, (parent_id,))
1839+
if relation.rel_type == RelationTypes.THREAD:
1840+
txn.call_after(
1841+
self.store.get_thread_summary.invalidate, (relation.parent_id,)
1842+
)
18501843
# It should be safe to only invalidate the cache if the user has not
18511844
# previously participated in the thread, but that's difficult (and
18521845
# potentially error-prone) so it is always invalidated.
18531846
txn.call_after(
18541847
self.store.get_thread_participated.invalidate,
1855-
(parent_id, event.sender),
1848+
(relation.parent_id, event.sender),
18561849
)
18571850

18581851
def _handle_insertion_event(

tests/rest/client/test_sync.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,12 +656,13 @@ def test_unread_counts(self) -> None:
656656
self._check_unread_count(3)
657657

658658
# Check that custom events with a body increase the unread counter.
659-
self.helper.send_event(
659+
result = self.helper.send_event(
660660
self.room_id,
661661
"org.matrix.custom_type",
662662
{"body": "hello"},
663663
tok=self.tok2,
664664
)
665+
event_id = result["event_id"]
665666
self._check_unread_count(4)
666667

667668
# Check that edits don't increase the unread counter.
@@ -671,7 +672,10 @@ def test_unread_counts(self) -> None:
671672
content={
672673
"body": "hello",
673674
"msgtype": "m.text",
674-
"m.relates_to": {"rel_type": RelationTypes.REPLACE},
675+
"m.relates_to": {
676+
"rel_type": RelationTypes.REPLACE,
677+
"event_id": event_id,
678+
},
675679
},
676680
tok=self.tok2,
677681
)

0 commit comments

Comments
 (0)