From 2296ac74a77940b2ceeaf4dbd3d82cfe3ca61731 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 13 Mar 2026 14:03:10 +0200 Subject: [PATCH 1/4] Fix join rule types Closes #182 --- mautrix/types/event/state.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mautrix/types/event/state.py b/mautrix/types/event/state.py index ef51cff4..5ffc855f 100644 --- a/mautrix/types/event/state.py +++ b/mautrix/types/event/state.py @@ -9,7 +9,7 @@ import attr from ..primitive import JSON, ContentURI, EventID, RoomAlias, RoomID, UserID -from ..util import Obj, SerializableAttrs, SerializableEnum, deserializer, field +from ..util import ExtensibleEnum, Obj, SerializableAttrs, SerializableEnum, deserializer, field from .base import BaseRoomEvent, BaseUnsigned from .encrypted import EncryptionAlgorithm from .type import EventType, RoomType @@ -159,16 +159,15 @@ class RoomAvatarStateEventContent(SerializableAttrs): url: Optional[ContentURI] = None -class JoinRule(SerializableEnum): +class JoinRule(ExtensibleEnum): PUBLIC = "public" KNOCK = "knock" RESTRICTED = "restricted" INVITE = "invite" - PRIVATE = "private" KNOCK_RESTRICTED = "knock_restricted" -class JoinRestrictionType(SerializableEnum): +class JoinRestrictionType(ExtensibleEnum): ROOM_MEMBERSHIP = "m.room_membership" From 89ebd5fff50ca22a8c6e4f554ad9aae393c96760 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Mar 2026 14:48:30 +0200 Subject: [PATCH 2/4] Fix parsing /messages response --- mautrix/client/api/events.py | 6 ++---- mautrix/types/misc.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mautrix/client/api/events.py b/mautrix/client/api/events.py index fd6b2c9d..bd415b9b 100644 --- a/mautrix/client/api/events.py +++ b/mautrix/client/api/events.py @@ -395,15 +395,13 @@ async def get_messages( try: return PaginatedMessages( content["start"], - content["end"], + content.get("end"), [Event.deserialize(event) for event in content["chunk"]], ) except KeyError: if "start" not in content: raise MatrixResponseError("`start` not in response.") - elif "end" not in content: - raise MatrixResponseError("`start` not in response.") - raise MatrixResponseError("`content` not in response.") + raise MatrixResponseError("`chunk` not in response.") except SerializerError as e: raise MatrixResponseError("Invalid events in response") from e diff --git a/mautrix/types/misc.py b/mautrix/types/misc.py index cf576c2b..5a07699c 100644 --- a/mautrix/types/misc.py +++ b/mautrix/types/misc.py @@ -106,7 +106,7 @@ class RoomDirectoryResponse(SerializableAttrs): PaginatedMessages = NamedTuple( - "PaginatedMessages", start=SyncToken, end=SyncToken, events=List[Event] + "PaginatedMessages", start=SyncToken, end=Optional[SyncToken], events=List[Event] ) From 8e0dcefb9f4b68f30b58031272f1048e36980422 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 12 Apr 2026 14:04:31 +0300 Subject: [PATCH 3/4] Fix transaction in abstract crypto store --- mautrix/crypto/store/abstract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mautrix/crypto/store/abstract.py b/mautrix/crypto/store/abstract.py index 1bae832f..0916a2d7 100644 --- a/mautrix/crypto/store/abstract.py +++ b/mautrix/crypto/store/abstract.py @@ -7,6 +7,7 @@ from typing import AsyncContextManager, NamedTuple from abc import ABC, abstractmethod +from contextlib import asynccontextmanager from mautrix.types import ( CrossSigner, @@ -87,9 +88,10 @@ async def close(self) -> None: async def flush(self) -> None: """Flush the store. If all the methods persist data immediately, this can be a no-op.""" - async def transaction(self) -> AsyncContextManager[None]: + @asynccontextmanager + async def transaction(self) -> None: """Run a database transaction. If the store doesn't support transactions, this can be a no-op.""" - pass + yield @abstractmethod async def delete(self) -> None: From bea9e63142c1fc89820be3344941cb57808456b6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 12 Apr 2026 14:10:39 +0300 Subject: [PATCH 4/4] Update black and isort --- .github/workflows/python-package.yml | 6 +- .pre-commit-config.yaml | 4 +- dev-requirements.txt | 4 +- mautrix/client/state_store/asyncpg/upgrade.py | 22 +-- mautrix/crypto/store/asyncpg/upgrade.py | 176 +++++++++--------- mautrix/util/async_db/database.py | 8 +- mautrix/util/async_db/upgrade.py | 8 +- 7 files changed, 109 insertions(+), 119 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e6f77c9f..3a845672 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,7 +11,7 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: @@ -46,7 +46,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.14" @@ -56,7 +56,7 @@ jobs: - uses: psf/black@stable with: src: "./mautrix" - version: "25.11.0" + version: "26.3.1" - name: pre-commit run: | pip install pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3bd2047..66065033 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,13 +8,13 @@ repos: - id: check-yaml - id: check-added-large-files - repo: https://github.com/psf/black - rev: 25.11.0 + rev: 26.3.1 hooks: - id: black language_version: python3 files: ^mautrix/.*\.pyi?$ - repo: https://github.com/PyCQA/isort - rev: 7.0.0 + rev: 8.0.1 hooks: - id: isort files: ^mautrix/.*\.pyi?$ diff --git a/dev-requirements.txt b/dev-requirements.txt index bb8c2a0a..dff6ee9d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,3 @@ pre-commit>=2.10.1,<3 -isort>=5.10.1,<6 -black>=24,<25 +isort>=8,<9 +black>=26,<27 diff --git a/mautrix/client/state_store/asyncpg/upgrade.py b/mautrix/client/state_store/asyncpg/upgrade.py index c1f32664..20b2e5b2 100644 --- a/mautrix/client/state_store/asyncpg/upgrade.py +++ b/mautrix/client/state_store/asyncpg/upgrade.py @@ -16,16 +16,16 @@ @upgrade_table.register(description="Latest revision", upgrades_to=4) async def upgrade_blank_to_v4(conn: Connection, scheme: Scheme) -> None: - await conn.execute( - """CREATE TABLE mx_room_state ( + await conn.execute(""" + CREATE TABLE mx_room_state ( room_id TEXT PRIMARY KEY, is_encrypted BOOLEAN, has_full_member_list BOOLEAN, encryption TEXT, power_levels TEXT, create_event TEXT - )""" - ) + ) + """) membership_check = "" if scheme != Scheme.SQLITE: await conn.execute( @@ -33,16 +33,16 @@ async def upgrade_blank_to_v4(conn: Connection, scheme: Scheme) -> None: ) else: membership_check = "CHECK (membership IN ('join', 'leave', 'invite', 'ban', 'knock'))" - await conn.execute( - f"""CREATE TABLE mx_user_profile ( + await conn.execute(f""" + CREATE TABLE mx_user_profile ( room_id TEXT, user_id TEXT, membership membership NOT NULL {membership_check}, displayname TEXT, avatar_url TEXT, PRIMARY KEY (room_id, user_id) - )""" - ) + ) + """) @upgrade_table.register(description="Stop using size-limited string fields") @@ -60,16 +60,14 @@ async def upgrade_v2(conn: Connection, scheme: Scheme) -> None: @upgrade_table.register(description="Mark rooms that need crypto state event resynced") async def upgrade_v3(conn: Connection) -> None: if await conn.table_exists("portal"): - await conn.execute( - """ + await conn.execute(""" INSERT INTO mx_room_state (room_id, encryption) SELECT portal.mxid, '{"resync":true}' FROM portal WHERE portal.encrypted=true AND portal.mxid IS NOT NULL ON CONFLICT (room_id) DO UPDATE SET encryption=excluded.encryption WHERE mx_room_state.encryption IS NULL - """ - ) + """) @upgrade_table.register(description="Add create event to room state cache") diff --git a/mautrix/crypto/store/asyncpg/upgrade.py b/mautrix/crypto/store/asyncpg/upgrade.py index e097c5d9..8d413858 100644 --- a/mautrix/crypto/store/asyncpg/upgrade.py +++ b/mautrix/crypto/store/asyncpg/upgrade.py @@ -18,32 +18,32 @@ @upgrade_table.register(description="Latest revision", upgrades_to=10) async def upgrade_blank_to_latest(conn: Connection) -> None: - await conn.execute( - """CREATE TABLE IF NOT EXISTS crypto_account ( + await conn.execute(""" + CREATE TABLE IF NOT EXISTS crypto_account ( account_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, shared BOOLEAN NOT NULL, sync_token TEXT NOT NULL, account bytea NOT NULL - )""" - ) - await conn.execute( - """CREATE TABLE IF NOT EXISTS crypto_message_index ( + ) + """) + await conn.execute(""" + CREATE TABLE IF NOT EXISTS crypto_message_index ( sender_key CHAR(43), session_id CHAR(43), "index" INTEGER, event_id TEXT NOT NULL, timestamp BIGINT NOT NULL, PRIMARY KEY (sender_key, session_id, "index") - )""" - ) - await conn.execute( - """CREATE TABLE IF NOT EXISTS crypto_tracked_user ( + ) + """) + await conn.execute(""" + CREATE TABLE IF NOT EXISTS crypto_tracked_user ( user_id TEXT PRIMARY KEY - )""" - ) - await conn.execute( - """CREATE TABLE IF NOT EXISTS crypto_device ( + ) + """) + await conn.execute(""" + CREATE TABLE IF NOT EXISTS crypto_device ( user_id TEXT, device_id TEXT, identity_key CHAR(43) NOT NULL, @@ -52,10 +52,10 @@ async def upgrade_blank_to_latest(conn: Connection) -> None: deleted BOOLEAN NOT NULL, name TEXT NOT NULL, PRIMARY KEY (user_id, device_id) - )""" - ) - await conn.execute( - """CREATE TABLE IF NOT EXISTS crypto_olm_session ( + ) + """) + await conn.execute(""" + CREATE TABLE IF NOT EXISTS crypto_olm_session ( account_id TEXT, session_id CHAR(43), sender_key CHAR(43) NOT NULL, @@ -64,10 +64,10 @@ async def upgrade_blank_to_latest(conn: Connection) -> None: last_decrypted timestamp NOT NULL, last_encrypted timestamp NOT NULL, PRIMARY KEY (account_id, session_id) - )""" - ) - await conn.execute( - """CREATE TABLE IF NOT EXISTS crypto_megolm_inbound_session ( + ) + """) + await conn.execute(""" + CREATE TABLE IF NOT EXISTS crypto_megolm_inbound_session ( account_id TEXT, session_id CHAR(43), sender_key CHAR(43) NOT NULL, @@ -83,10 +83,10 @@ async def upgrade_blank_to_latest(conn: Connection) -> None: max_messages INTEGER, is_scheduled BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY (account_id, session_id) - )""" - ) - await conn.execute( - """CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session ( + ) + """) + await conn.execute(""" + CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session ( account_id TEXT, room_id TEXT, session_id CHAR(43) NOT NULL UNIQUE, @@ -98,10 +98,10 @@ async def upgrade_blank_to_latest(conn: Connection) -> None: created_at timestamp NOT NULL, last_used timestamp NOT NULL, PRIMARY KEY (account_id, room_id) - )""" - ) - await conn.execute( - """CREATE TABLE crypto_cross_signing_keys ( + ) + """) + await conn.execute(""" + CREATE TABLE crypto_cross_signing_keys ( user_id TEXT, usage TEXT, key CHAR(43) NOT NULL, @@ -109,18 +109,18 @@ async def upgrade_blank_to_latest(conn: Connection) -> None: first_seen_key CHAR(43) NOT NULL, PRIMARY KEY (user_id, usage) - )""" - ) - await conn.execute( - """CREATE TABLE crypto_cross_signing_signatures ( + ) + """) + await conn.execute(""" + CREATE TABLE crypto_cross_signing_signatures ( signed_user_id TEXT, signed_key TEXT, signer_user_id TEXT, signer_key TEXT, signature CHAR(88) NOT NULL, PRIMARY KEY (signed_user_id, signed_key, signer_user_id, signer_key) - )""" - ) + ) + """) @upgrade_table.register(description="Add account_id primary key column") @@ -130,17 +130,17 @@ async def upgrade_v2(conn: Connection, scheme: Scheme) -> None: await conn.execute("DROP TABLE crypto_olm_session") await conn.execute("DROP TABLE crypto_megolm_inbound_session") await conn.execute("DROP TABLE crypto_megolm_outbound_session") - await conn.execute( - """CREATE TABLE crypto_account ( + await conn.execute(""" + CREATE TABLE crypto_account ( account_id VARCHAR(255) PRIMARY KEY, device_id VARCHAR(255) NOT NULL, shared BOOLEAN NOT NULL, sync_token TEXT NOT NULL, account bytea NOT NULL - )""" - ) - await conn.execute( - """CREATE TABLE crypto_olm_session ( + ) + """) + await conn.execute(""" + CREATE TABLE crypto_olm_session ( account_id VARCHAR(255), session_id CHAR(43), sender_key CHAR(43) NOT NULL, @@ -148,10 +148,10 @@ async def upgrade_v2(conn: Connection, scheme: Scheme) -> None: created_at timestamp NOT NULL, last_used timestamp NOT NULL, PRIMARY KEY (account_id, session_id) - )""" - ) - await conn.execute( - """CREATE TABLE crypto_megolm_inbound_session ( + ) + """) + await conn.execute(""" + CREATE TABLE crypto_megolm_inbound_session ( account_id VARCHAR(255), session_id CHAR(43), sender_key CHAR(43) NOT NULL, @@ -160,10 +160,10 @@ async def upgrade_v2(conn: Connection, scheme: Scheme) -> None: session bytea NOT NULL, forwarding_chains TEXT NOT NULL, PRIMARY KEY (account_id, session_id) - )""" - ) - await conn.execute( - """CREATE TABLE crypto_megolm_outbound_session ( + ) + """) + await conn.execute(""" + CREATE TABLE crypto_megolm_outbound_session ( account_id VARCHAR(255), room_id VARCHAR(255), session_id CHAR(43) NOT NULL UNIQUE, @@ -175,8 +175,8 @@ async def upgrade_v2(conn: Connection, scheme: Scheme) -> None: created_at timestamp NOT NULL, last_used timestamp NOT NULL, PRIMARY KEY (account_id, room_id) - )""" - ) + ) + """) else: async def add_account_id_column(table: str, pkey_columns: list[str]) -> None: @@ -233,25 +233,25 @@ async def upgrade_v4(conn: Connection, scheme: Scheme) -> None: @upgrade_table.register(description="Add cross-signing key and signature caches") async def upgrade_v5(conn: Connection) -> None: - await conn.execute( - """CREATE TABLE crypto_cross_signing_keys ( + await conn.execute(""" + CREATE TABLE crypto_cross_signing_keys ( user_id TEXT, usage TEXT, key CHAR(43), first_seen_key CHAR(43), PRIMARY KEY (user_id, usage) - )""" - ) - await conn.execute( - """CREATE TABLE crypto_cross_signing_signatures ( + ) + """) + await conn.execute(""" + CREATE TABLE crypto_cross_signing_signatures ( signed_user_id TEXT, signed_key TEXT, signer_user_id TEXT, signer_key TEXT, signature TEXT, PRIMARY KEY (signed_user_id, signed_key, signer_user_id, signer_key) - )""" - ) + ) + """) @upgrade_table.register(description="Update trust state values") @@ -322,27 +322,25 @@ async def upgrade_v9_postgres(conn: Connection) -> None: async def upgrade_v9_sqlite(conn: Connection) -> None: await conn.execute("PRAGMA foreign_keys = OFF") async with conn.transaction(): - await conn.execute( - """CREATE TABLE new_crypto_account ( + await conn.execute(""" + CREATE TABLE new_crypto_account ( account_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, shared BOOLEAN NOT NULL, sync_token TEXT NOT NULL, account bytea NOT NULL - )""" - ) - await conn.execute( - """ + ) + """) + await conn.execute(""" INSERT INTO new_crypto_account (account_id, device_id, shared, sync_token, account) SELECT account_id, COALESCE(device_id, ''), shared, sync_token, account FROM crypto_account - """ - ) + """) await conn.execute("DROP TABLE crypto_account") await conn.execute("ALTER TABLE new_crypto_account RENAME TO crypto_account") - await conn.execute( - """CREATE TABLE new_crypto_megolm_inbound_session ( + await conn.execute(""" + CREATE TABLE new_crypto_megolm_inbound_session ( account_id TEXT, session_id CHAR(43), sender_key CHAR(43) NOT NULL, @@ -353,10 +351,9 @@ async def upgrade_v9_sqlite(conn: Connection) -> None: withheld_code TEXT, withheld_reason TEXT, PRIMARY KEY (account_id, session_id) - )""" - ) - await conn.execute( - """ + ) + """) + await conn.execute(""" INSERT INTO new_crypto_megolm_inbound_session ( account_id, session_id, sender_key, signing_key, room_id, session, forwarding_chains @@ -364,8 +361,7 @@ async def upgrade_v9_sqlite(conn: Connection) -> None: SELECT account_id, session_id, sender_key, signing_key, room_id, session, forwarding_chains FROM crypto_megolm_inbound_session - """ - ) + """) await conn.execute("DROP TABLE crypto_megolm_inbound_session") await conn.execute( "ALTER TABLE new_crypto_megolm_inbound_session RENAME TO crypto_megolm_inbound_session" @@ -373,8 +369,8 @@ async def upgrade_v9_sqlite(conn: Connection) -> None: await conn.execute("UPDATE crypto_megolm_outbound_session SET max_age=max_age*1000") - await conn.execute( - """CREATE TABLE new_crypto_cross_signing_keys ( + await conn.execute(""" + CREATE TABLE new_crypto_cross_signing_keys ( user_id TEXT, usage TEXT, key CHAR(43) NOT NULL, @@ -382,41 +378,37 @@ async def upgrade_v9_sqlite(conn: Connection) -> None: first_seen_key CHAR(43) NOT NULL, PRIMARY KEY (user_id, usage) - )""" - ) - await conn.execute( - """ + ) + """) + await conn.execute(""" INSERT INTO new_crypto_cross_signing_keys (user_id, usage, key, first_seen_key) SELECT user_id, usage, key, COALESCE(first_seen_key, key) FROM crypto_cross_signing_keys WHERE key IS NOT NULL - """ - ) + """) await conn.execute("DROP TABLE crypto_cross_signing_keys") await conn.execute( "ALTER TABLE new_crypto_cross_signing_keys RENAME TO crypto_cross_signing_keys" ) - await conn.execute( - """CREATE TABLE new_crypto_cross_signing_signatures ( + await conn.execute(""" + CREATE TABLE new_crypto_cross_signing_signatures ( signed_user_id TEXT, signed_key TEXT, signer_user_id TEXT, signer_key TEXT, signature CHAR(88) NOT NULL, PRIMARY KEY (signed_user_id, signed_key, signer_user_id, signer_key) - )""" - ) - await conn.execute( - """ + ) + """) + await conn.execute(""" INSERT INTO new_crypto_cross_signing_signatures ( signed_user_id, signed_key, signer_user_id, signer_key, signature ) SELECT signed_user_id, signed_key, signer_user_id, signer_key, signature FROM crypto_cross_signing_signatures WHERE signature IS NOT NULL - """ - ) + """) await conn.execute("DROP TABLE crypto_cross_signing_signatures") await conn.execute( "ALTER TABLE new_crypto_cross_signing_signatures " diff --git a/mautrix/util/async_db/database.py b/mautrix/util/async_db/database.py index 6a17dc68..b5128b74 100644 --- a/mautrix/util/async_db/database.py +++ b/mautrix/util/async_db/database.py @@ -115,12 +115,12 @@ async def _check_foreign_tables(self) -> None: raise ForeignTablesFound("found roomserver_rooms likely belonging to Dendrite") async def _check_owner(self) -> None: - await self.execute( - """CREATE TABLE IF NOT EXISTS database_owner ( + await self.execute(""" + CREATE TABLE IF NOT EXISTS database_owner ( key INTEGER PRIMARY KEY DEFAULT 0, owner TEXT NOT NULL - )""" - ) + ) + """) owner = await self.fetchval("SELECT owner FROM database_owner WHERE key=0") if not owner: await self.execute( diff --git a/mautrix/util/async_db/upgrade.py b/mautrix/util/async_db/upgrade.py index 9e593ece..c084d28b 100644 --- a/mautrix/util/async_db/upgrade.py +++ b/mautrix/util/async_db/upgrade.py @@ -97,11 +97,11 @@ async def _save_version(self, conn: LoggingConnection, version: int) -> None: await conn.execute(f"INSERT INTO {self.version_table_name} (version) VALUES ($1)", version) async def upgrade(self, db: async_db.Database) -> None: - await db.execute( - f"""CREATE TABLE IF NOT EXISTS {self.version_table_name} ( + await db.execute(f""" + CREATE TABLE IF NOT EXISTS {self.version_table_name} ( version INTEGER PRIMARY KEY - )""" - ) + ) + """) row = await db.fetchrow(f"SELECT version FROM {self.version_table_name} LIMIT 1") version = row["version"] if row else 0