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

Commit 45f4540

Browse files
authored
Fix incorrect thread summaries when the latest event is edited. (#11992)
If the latest event in a thread was edited than the original event content was included in bundled aggregation for threads instead of the edited event content.
1 parent 85e24d9 commit 45f4540

File tree

5 files changed

+107
-31
lines changed

5 files changed

+107
-31
lines changed

changelog.d/11992.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug introduced in Synapse v1.48.0 where an edit of the latest event in a thread would not be properly applied to the thread summary.

synapse/events/utils.py

+45-24
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,33 @@ def serialize_event(
425425

426426
return serialized_event
427427

428+
def _apply_edit(
429+
self, orig_event: EventBase, serialized_event: JsonDict, edit: EventBase
430+
) -> None:
431+
"""Replace the content, preserving existing relations of the serialized event.
432+
433+
Args:
434+
orig_event: The original event.
435+
serialized_event: The original event, serialized. This is modified.
436+
edit: The event which edits the above.
437+
"""
438+
439+
# Ensure we take copies of the edit content, otherwise we risk modifying
440+
# the original event.
441+
edit_content = edit.content.copy()
442+
443+
# Unfreeze the event content if necessary, so that we may modify it below
444+
edit_content = unfreeze(edit_content)
445+
serialized_event["content"] = edit_content.get("m.new_content", {})
446+
447+
# Check for existing relations
448+
relates_to = orig_event.content.get("m.relates_to")
449+
if relates_to:
450+
# Keep the relations, ensuring we use a dict copy of the original
451+
serialized_event["content"]["m.relates_to"] = relates_to.copy()
452+
else:
453+
serialized_event["content"].pop("m.relates_to", None)
454+
428455
def _inject_bundled_aggregations(
429456
self,
430457
event: EventBase,
@@ -450,26 +477,11 @@ def _inject_bundled_aggregations(
450477
serialized_aggregations[RelationTypes.REFERENCE] = aggregations.references
451478

452479
if aggregations.replace:
453-
# If there is an edit replace the content, preserving existing
454-
# relations.
480+
# If there is an edit, apply it to the event.
455481
edit = aggregations.replace
482+
self._apply_edit(event, serialized_event, edit)
456483

457-
# Ensure we take copies of the edit content, otherwise we risk modifying
458-
# the original event.
459-
edit_content = edit.content.copy()
460-
461-
# Unfreeze the event content if necessary, so that we may modify it below
462-
edit_content = unfreeze(edit_content)
463-
serialized_event["content"] = edit_content.get("m.new_content", {})
464-
465-
# Check for existing relations
466-
relates_to = event.content.get("m.relates_to")
467-
if relates_to:
468-
# Keep the relations, ensuring we use a dict copy of the original
469-
serialized_event["content"]["m.relates_to"] = relates_to.copy()
470-
else:
471-
serialized_event["content"].pop("m.relates_to", None)
472-
484+
# Include information about it in the relations dict.
473485
serialized_aggregations[RelationTypes.REPLACE] = {
474486
"event_id": edit.event_id,
475487
"origin_server_ts": edit.origin_server_ts,
@@ -478,13 +490,22 @@ def _inject_bundled_aggregations(
478490

479491
# If this event is the start of a thread, include a summary of the replies.
480492
if aggregations.thread:
493+
thread = aggregations.thread
494+
495+
# Don't bundle aggregations as this could recurse forever.
496+
serialized_latest_event = self.serialize_event(
497+
thread.latest_event, time_now, bundle_aggregations=None
498+
)
499+
# Manually apply an edit, if one exists.
500+
if thread.latest_edit:
501+
self._apply_edit(
502+
thread.latest_event, serialized_latest_event, thread.latest_edit
503+
)
504+
481505
serialized_aggregations[RelationTypes.THREAD] = {
482-
# Don't bundle aggregations as this could recurse forever.
483-
"latest_event": self.serialize_event(
484-
aggregations.thread.latest_event, time_now, bundle_aggregations=None
485-
),
486-
"count": aggregations.thread.count,
487-
"current_user_participated": aggregations.thread.current_user_participated,
506+
"latest_event": serialized_latest_event,
507+
"count": thread.count,
508+
"current_user_participated": thread.current_user_participated,
488509
}
489510

490511
# Include the bundled aggregations in the event.

synapse/storage/databases/main/events_worker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ async def get_events(
408408
include the previous states content in the unsigned field.
409409
410410
allow_rejected: If True, return rejected events. Otherwise,
411-
omits rejeted events from the response.
411+
omits rejected events from the response.
412412
413413
Returns:
414414
A mapping from event_id to event.

synapse/storage/databases/main/relations.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,13 @@
5353

5454
@attr.s(slots=True, frozen=True, auto_attribs=True)
5555
class _ThreadAggregation:
56+
# The latest event in the thread.
5657
latest_event: EventBase
58+
# The latest edit to the latest event in the thread.
59+
latest_edit: Optional[EventBase]
60+
# The total number of events in the thread.
5761
count: int
62+
# True if the current user has sent an event to the thread.
5863
current_user_participated: bool
5964

6065

@@ -461,8 +466,8 @@ def get_thread_summary(self, event_id: str) -> Optional[Tuple[int, EventBase]]:
461466
@cachedList(cached_method_name="get_thread_summary", list_name="event_ids")
462467
async def _get_thread_summaries(
463468
self, event_ids: Collection[str]
464-
) -> Dict[str, Optional[Tuple[int, EventBase]]]:
465-
"""Get the number of threaded replies and the latest reply (if any) for the given event.
469+
) -> Dict[str, Optional[Tuple[int, EventBase, Optional[EventBase]]]]:
470+
"""Get the number of threaded replies, the latest reply (if any), and the latest edit for that reply for the given event.
466471
467472
Args:
468473
event_ids: Summarize the thread related to this event ID.
@@ -471,8 +476,10 @@ async def _get_thread_summaries(
471476
A map of the thread summary each event. A missing event implies there
472477
are no threaded replies.
473478
474-
Each summary includes the number of items in the thread and the most
475-
recent response.
479+
Each summary is a tuple of:
480+
The number of events in the thread.
481+
The most recent event in the thread.
482+
The most recent edit to the most recent event in the thread, if applicable.
476483
"""
477484

478485
def _get_thread_summaries_txn(
@@ -558,6 +565,9 @@ def _get_thread_summaries_txn(
558565

559566
latest_events = await self.get_events(latest_event_ids.values()) # type: ignore[attr-defined]
560567

568+
# Check to see if any of those events are edited.
569+
latest_edits = await self._get_applicable_edits(latest_event_ids.values())
570+
561571
# Map to the event IDs to the thread summary.
562572
#
563573
# There might not be a summary due to there not being a thread or
@@ -568,7 +578,8 @@ def _get_thread_summaries_txn(
568578

569579
summary = None
570580
if latest_event:
571-
summary = (counts[parent_event_id], latest_event)
581+
latest_edit = latest_edits.get(latest_event_id)
582+
summary = (counts[parent_event_id], latest_event, latest_edit)
572583
summaries[parent_event_id] = summary
573584

574585
return summaries
@@ -828,11 +839,12 @@ async def get_bundled_aggregations(
828839
)
829840
for event_id, summary in summaries.items():
830841
if summary:
831-
thread_count, latest_thread_event = summary
842+
thread_count, latest_thread_event, edit = summary
832843
results.setdefault(
833844
event_id, BundledAggregations()
834845
).thread = _ThreadAggregation(
835846
latest_event=latest_thread_event,
847+
latest_edit=edit,
836848
count=thread_count,
837849
# If there's a thread summary it must also exist in the
838850
# participated dictionary.

tests/rest/client/test_relations.py

+42
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,48 @@ def test_edit_reply(self):
11231123
{"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict
11241124
)
11251125

1126+
@unittest.override_config({"experimental_features": {"msc3440_enabled": True}})
1127+
def test_edit_thread(self):
1128+
"""Test that editing a thread works."""
1129+
1130+
# Create a thread and edit the last event.
1131+
channel = self._send_relation(
1132+
RelationTypes.THREAD,
1133+
"m.room.message",
1134+
content={"msgtype": "m.text", "body": "A threaded reply!"},
1135+
)
1136+
self.assertEquals(200, channel.code, channel.json_body)
1137+
threaded_event_id = channel.json_body["event_id"]
1138+
1139+
new_body = {"msgtype": "m.text", "body": "I've been edited!"}
1140+
channel = self._send_relation(
1141+
RelationTypes.REPLACE,
1142+
"m.room.message",
1143+
content={"msgtype": "m.text", "body": "foo", "m.new_content": new_body},
1144+
parent_id=threaded_event_id,
1145+
)
1146+
self.assertEquals(200, channel.code, channel.json_body)
1147+
1148+
# Fetch the thread root, to get the bundled aggregation for the thread.
1149+
channel = self.make_request(
1150+
"GET",
1151+
f"/rooms/{self.room}/event/{self.parent_id}",
1152+
access_token=self.user_token,
1153+
)
1154+
self.assertEquals(200, channel.code, channel.json_body)
1155+
1156+
# We expect that the edit message appears in the thread summary in the
1157+
# unsigned relations section.
1158+
relations_dict = channel.json_body["unsigned"].get("m.relations")
1159+
self.assertIn(RelationTypes.THREAD, relations_dict)
1160+
1161+
thread_summary = relations_dict[RelationTypes.THREAD]
1162+
self.assertIn("latest_event", thread_summary)
1163+
latest_event_in_thread = thread_summary["latest_event"]
1164+
self.assertEquals(
1165+
latest_event_in_thread["content"]["body"], "I've been edited!"
1166+
)
1167+
11261168
def test_edit_edit(self):
11271169
"""Test that an edit cannot be edited."""
11281170
new_body = {"msgtype": "m.text", "body": "Initial edit"}

0 commit comments

Comments
 (0)