mirror of
https://github.com/element-hq/synapse.git
synced 2026-03-29 11:00:30 +00:00
MSC4140: delayed event content as text, not bytes (#19360)
Store the JSON content of scheduled delayed events as text instead of a byte array. This brings it in line with the `event_json` table's `json` column, and fixes the inability to schedule a delayed event with non-ASCII characters in its content. Fixes #19242
This commit is contained in:
committed by
GitHub
parent
a1e9abc7df
commit
079c52e16b
1
changelog.d/19360.bugfix
Normal file
1
changelog.d/19360.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
MSC4140: Store the JSON content of scheduled delayed events as text instead of a byte array. This fixes the inability to schedule a delayed event with non-ASCII characters in its content.
|
||||
@@ -187,6 +187,14 @@ def check_schema_delta(delta_files: list[str], force_colors: bool) -> bool:
|
||||
sql_lang = "postgres"
|
||||
if delta_file.endswith(".sqlite"):
|
||||
sql_lang = "sqlite"
|
||||
elif delta_file.endswith(".py"):
|
||||
click.secho(
|
||||
f"Skipping Python delta file: '{delta_file}'",
|
||||
fg="yellow",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
return True
|
||||
|
||||
statements = sqlglot.parse(delta_contents, read=sql_lang)
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2026 Element Creations 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>.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from synapse.storage.database import LoggingTransaction
|
||||
from synapse.storage.engines import BaseDatabaseEngine, PostgresEngine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_create(cur: LoggingTransaction, database_engine: BaseDatabaseEngine) -> None:
|
||||
"""
|
||||
Change the type / affinity of the `delayed_events` table's `content` column from bytes to text.
|
||||
This brings it in line with the `event_json` table's `json` column, and fixes the inability to
|
||||
schedule a delayed event with non-ASCII characters in its content.
|
||||
"""
|
||||
|
||||
if isinstance(database_engine, PostgresEngine):
|
||||
cur.execute(
|
||||
"ALTER TABLE delayed_events "
|
||||
"ALTER COLUMN content SET DATA TYPE TEXT "
|
||||
"USING convert_from(content, 'utf8')"
|
||||
)
|
||||
return
|
||||
|
||||
# For sqlite3, change the type affinity by fiddling with the table schema directly.
|
||||
# This strategy is also used by ../50/make_event_content_nullable.py.
|
||||
|
||||
cur.execute(
|
||||
"SELECT sql FROM sqlite_master WHERE tbl_name='delayed_events' AND type='table'"
|
||||
)
|
||||
row = cur.fetchone()
|
||||
assert row is not None
|
||||
(oldsql,) = row
|
||||
|
||||
sql = oldsql.replace("content bytea", "content TEXT")
|
||||
if sql == oldsql:
|
||||
raise Exception("Couldn't find content bytes column in %s" % oldsql)
|
||||
|
||||
cur.execute("PRAGMA schema_version")
|
||||
row = cur.fetchone()
|
||||
assert row is not None
|
||||
(oldver,) = row
|
||||
cur.execute("PRAGMA writable_schema=ON")
|
||||
cur.execute(
|
||||
"UPDATE sqlite_master SET sql=? WHERE tbl_name='delayed_events' AND type='table'",
|
||||
(sql,),
|
||||
)
|
||||
cur.execute("PRAGMA schema_version=%i" % (oldver + 1,))
|
||||
cur.execute("PRAGMA writable_schema=OFF")
|
||||
@@ -278,17 +278,24 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
channel = self._update_delayed_event(delay_ids.pop(0), "cancel", action_in_path)
|
||||
self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result)
|
||||
|
||||
@parameterized.expand((True, False))
|
||||
def test_send_delayed_state_event(self, action_in_path: bool) -> None:
|
||||
@parameterized.expand(
|
||||
(
|
||||
(content_property_value, action_in_path)
|
||||
for content_property_value in ("test", "tест")
|
||||
for action_in_path in (True, False)
|
||||
)
|
||||
)
|
||||
def test_send_delayed_state_event(
|
||||
self, content_value: str, action_in_path: bool
|
||||
) -> None:
|
||||
state_key = "to_send_on_request"
|
||||
|
||||
setter_key = "setter"
|
||||
setter_expected = "on_send"
|
||||
content_property_name = "key"
|
||||
channel = self.make_request(
|
||||
"PUT",
|
||||
_get_path_for_delayed_state(self.room_id, _EVENT_TYPE, state_key, 100000),
|
||||
{
|
||||
setter_key: setter_expected,
|
||||
content_property_name: content_value,
|
||||
},
|
||||
self.user1_access_token,
|
||||
)
|
||||
@@ -300,7 +307,7 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
events = self._get_delayed_events()
|
||||
self.assertEqual(1, len(events), events)
|
||||
content = self._get_delayed_event_content(events[0])
|
||||
self.assertEqual(setter_expected, content.get(setter_key), content)
|
||||
self.assertEqual(content_value, content.get(content_property_name), content)
|
||||
self.helper.get_state(
|
||||
self.room_id,
|
||||
_EVENT_TYPE,
|
||||
@@ -318,7 +325,7 @@ class DelayedEventsTestCase(HomeserverTestCase):
|
||||
self.user1_access_token,
|
||||
state_key=state_key,
|
||||
)
|
||||
self.assertEqual(setter_expected, content.get(setter_key), content)
|
||||
self.assertEqual(content_value, content.get(content_property_name), content)
|
||||
|
||||
@parameterized.expand((True, False))
|
||||
@unittest.override_config({"rc_message": {"per_second": 2.5, "burst_count": 3}})
|
||||
|
||||
Reference in New Issue
Block a user