This commit is contained in:
Johannes Marbach 2025-03-13 23:51:46 +01:00 committed by GitHub
commit 3ac39a212e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 196 additions and 4 deletions

View file

@ -0,0 +1 @@
Add plain-text handling for rich-text topics as per [MSC3765](https://github.com/matrix-org/matrix-spec-proposals/pull/3765).

View file

@ -1296,7 +1296,7 @@ class RoomCreationHandler:
topic = room_config["topic"]
topic_event, topic_context = await create_event(
EventTypes.Topic,
{"topic": topic},
{"topic": topic, "m.topic": {"m.text": [{"body": topic}]}},
True,
)
events_to_send.append((topic_event, topic_context))

View file

@ -36,6 +36,7 @@ from synapse.metrics import event_processing_positions
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.storage.databases.main.state_deltas import StateDelta
from synapse.types import JsonDict
from synapse.util.events import get_plain_text_topic_from_event_content
if TYPE_CHECKING:
from synapse.server import HomeServer
@ -299,7 +300,9 @@ class StatsHandler:
elif delta.event_type == EventTypes.Name:
room_state["name"] = event_content.get("name")
elif delta.event_type == EventTypes.Topic:
room_state["topic"] = event_content.get("topic")
room_state["topic"] = get_plain_text_topic_from_event_content(
event_content
)
elif delta.event_type == EventTypes.RoomAvatar:
room_state["avatar"] = event_content.get("url")
elif delta.event_type == EventTypes.CanonicalAlias:

View file

@ -78,6 +78,7 @@ from synapse.types import (
from synapse.types.handlers import SLIDING_SYNC_DEFAULT_BUMP_EVENT_TYPES
from synapse.types.state import StateFilter
from synapse.util import json_encoder
from synapse.util.events import get_plain_text_topic_from_event_content
from synapse.util.iterutils import batch_iter, sorted_topologically
from synapse.util.stringutils import non_null_str_or_none
@ -3102,7 +3103,10 @@ class PersistEventsStore:
def _store_room_topic_txn(self, txn: LoggingTransaction, event: EventBase) -> None:
if isinstance(event.content.get("topic"), str):
self.store_event_search_txn(
txn, event, "content.topic", event.content["topic"]
txn,
event,
"content.topic",
get_plain_text_topic_from_event_content(event.content) or "",
)
def _store_room_name_txn(self, txn: LoggingTransaction, event: EventBase) -> None:

View file

@ -48,6 +48,7 @@ from synapse.storage.databases.main.events_worker import InvalidEventError
from synapse.storage.databases.main.state_deltas import StateDeltasStore
from synapse.types import JsonDict
from synapse.util.caches.descriptors import cached
from synapse.util.events import get_plain_text_topic_from_event_content
if TYPE_CHECKING:
from synapse.server import HomeServer
@ -611,7 +612,9 @@ class StatsStore(StateDeltasStore):
elif event.type == EventTypes.Name:
room_state["name"] = event.content.get("name")
elif event.type == EventTypes.Topic:
room_state["topic"] = event.content.get("topic")
room_state["topic"] = get_plain_text_topic_from_event_content(
event.content
)
elif event.type == EventTypes.RoomAvatar:
room_state["avatar"] = event.content.get("url")
elif event.type == EventTypes.CanonicalAlias:

View file

@ -13,6 +13,9 @@
#
#
from typing import Optional
from synapse.types import JsonDict
from synapse.util.stringutils import random_string
@ -27,3 +30,31 @@ def generate_fake_event_id() -> str:
A string intended to look like an event ID, but with no actual meaning.
"""
return "$" + random_string(43)
def get_plain_text_topic_from_event_content(content: JsonDict) -> Optional[str]:
"""
Given the content of an m.room.topic event returns the plain text topic
representation if any exists.
Returns:
A string representing the plain text topic.
"""
topic = content.get("topic")
m_topic = content.get("m.topic")
if not m_topic:
return topic
m_text = m_topic.get("m.text")
if not m_text:
return topic
representation = next(
(r for r in m_text if "mimetype" not in r or r["mimetype"] == "text/plain"),
None,
)
if not representation or "body" not in representation:
return topic
return representation["body"]

View file

@ -757,6 +757,59 @@ class RoomsCreateTestCase(RoomBase):
assert channel.resource_usage is not None
self.assertEqual(37, channel.resource_usage.db_txn_count)
def test_post_room_topic(self) -> None:
# POST with topic key, expect new room id
channel = self.make_request("POST", "/createRoom", b'{"topic":"shenanigans"}')
self.assertEqual(HTTPStatus.OK, channel.code)
self.assertTrue("room_id" in channel.json_body)
room_id = channel.json_body["room_id"]
# GET topic event, expect content from topic key
channel = self.make_request("GET", "/rooms/%s/state/m.room.topic" % (room_id,))
self.assertEqual(HTTPStatus.OK, channel.code)
self.assertEqual(
{"topic": "shenanigans", "m.topic": {"m.text": [{"body": "shenanigans"}]}},
channel.json_body,
)
def test_post_room_topic_initial_state(self) -> None:
# POST with m.room.topic in initial state, expect new room id
channel = self.make_request(
"POST",
"/createRoom",
b'{"initial_state":[{"type": "m.room.topic", "content": {"topic": "foobar"}}]}',
)
self.assertEqual(HTTPStatus.OK, channel.code)
self.assertTrue("room_id" in channel.json_body)
room_id = channel.json_body["room_id"]
# GET topic event, expect content from initial state
channel = self.make_request("GET", "/rooms/%s/state/m.room.topic" % (room_id,))
self.assertEqual(HTTPStatus.OK, channel.code)
self.assertEqual(
{"topic": "foobar"},
channel.json_body,
)
def test_post_room_topic_overriding_initial_state(self) -> None:
# POST with m.room.topic in initial state and topic key, expect new room id
channel = self.make_request(
"POST",
"/createRoom",
b'{"initial_state":[{"type": "m.room.topic", "content": {"topic": "foobar"}}], "topic":"shenanigans"}',
)
self.assertEqual(HTTPStatus.OK, channel.code)
self.assertTrue("room_id" in channel.json_body)
room_id = channel.json_body["room_id"]
# GET topic event, expect content from topic key
channel = self.make_request("GET", "/rooms/%s/state/m.room.topic" % (room_id,))
self.assertEqual(HTTPStatus.OK, channel.code)
self.assertEqual(
{"topic": "shenanigans", "m.topic": {"m.text": [{"body": "shenanigans"}]}},
channel.json_body,
)
def test_post_room_visibility_key(self) -> None:
# POST with visibility config key, expect new room id
channel = self.make_request("POST", "/createRoom", b'{"visibility":"private"}')

97
tests/util/test_events.py Normal file
View file

@ -0,0 +1,97 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2025 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
# Originally licensed under the Apache License, Version 2.0:
# <http://www.apache.org/licenses/LICENSE-2.0>.
#
# [This file includes modifications made by New Vector Limited]
#
#
from synapse.util.events import get_plain_text_topic_from_event_content
from tests import unittest
class EventsTestCase(unittest.TestCase):
def test_get_plain_text_topic_no_topic(self) -> None:
# No legacy or rich topic, expect None
topic = get_plain_text_topic_from_event_content({})
self.assertEqual(None, topic)
def test_get_plain_text_topic_no_rich_topic(self) -> None:
# Only legacy topic, expect legacy topic
topic = get_plain_text_topic_from_event_content({"topic": "shenanigans"})
self.assertEqual("shenanigans", topic)
def test_get_plain_text_topic_rich_topic_without_representations(self) -> None:
# Legacy topic and rich topic without representations, expect legacy topic
topic = get_plain_text_topic_from_event_content(
{"topic": "shenanigans", "m.topic": {"m.text": []}}
)
self.assertEqual("shenanigans", topic)
def test_get_plain_text_topic_rich_topic_without_plain_text_representation(
self,
) -> None:
# Legacy topic and rich topic without plain text representation, expect legacy topic
topic = get_plain_text_topic_from_event_content(
{
"topic": "shenanigans",
"m.topic": {
"m.text": [
{"mimetype": "text/html", "body": "<strong>foobar</strong>"}
]
},
}
)
self.assertEqual("shenanigans", topic)
def test_get_plain_text_topic_rich_topic_with_plain_text_representation(
self,
) -> None:
# Legacy topic and rich topic with plain text representation, expect plain text representation
topic = get_plain_text_topic_from_event_content(
{
"topic": "shenanigans",
"m.topic": {"m.text": [{"mimetype": "text/plain", "body": "foobar"}]},
}
)
self.assertEqual("foobar", topic)
def test_get_plain_text_topic_rich_topic_with_implicit_plain_text_representation(
self,
) -> None:
# Legacy topic and rich topic with implicit plain text representation, expect plain text representation
topic = get_plain_text_topic_from_event_content(
{"topic": "shenanigans", "m.topic": {"m.text": [{"body": "foobar"}]}}
)
self.assertEqual("foobar", topic)
def test_get_plain_text_topic_rich_topic_with_plain_text_and_other_representation(
self,
) -> None:
# Legacy topic and rich topic with plain text representation, expect plain text representation
topic = get_plain_text_topic_from_event_content(
{
"topic": "shenanigans",
"m.topic": {
"m.text": [
{"mimetype": "text/html", "body": "<strong>foobar</strong>"},
{"mimetype": "text/plain", "body": "foobar"},
]
},
}
)
self.assertEqual("foobar", topic)