Skip to content

Commit 64e597b

Browse files
committed
Require Matrix v1.1 support in bridges
1 parent e3d1e53 commit 64e597b

File tree

5 files changed

+158
-21
lines changed

5 files changed

+158
-21
lines changed

mautrix/bridge/matrix.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@
4141
RoomID,
4242
RoomType,
4343
SingleReceiptEventContent,
44+
SpecVersions,
4445
StateEvent,
4546
StateUnsigned,
4647
TextMessageEventContent,
4748
TypingEvent,
4849
UserID,
50+
Version,
4951
VersionsResponse,
5052
)
5153
from mautrix.util import markdown
@@ -95,6 +97,7 @@ class BaseMatrixHandler:
9597
e2ee: EncryptionManager | None
9698
media_config: MediaRepoConfig
9799
versions: VersionsResponse
100+
minimum_spec_version: Version = SpecVersions.V11
98101

99102
user_id_prefix: str
100103
user_id_suffix: str
@@ -109,7 +112,7 @@ def __init__(
109112
self.bridge = bridge
110113
self.commands = command_processor or cmd.CommandProcessor(bridge=bridge)
111114
self.media_config = MediaRepoConfig(upload_size=50 * 1024 * 1024)
112-
self.versions = VersionsResponse(versions=["v1.2"])
115+
self.versions = VersionsResponse.deserialize({"versions": ["v1.3"]})
113116
self.az.matrix_event_handler(self.int_handle_event)
114117

115118
self.e2ee = None
@@ -149,13 +152,24 @@ def __init__(
149152
False,
150153
)
151154

155+
async def check_versions(self) -> None:
156+
if not self.versions.supports_at_least(self.minimum_spec_version):
157+
self.log.fatal(
158+
"Server isn't advertising modern spec versions "
159+
"(latest supported by server: %s, minimum required by bridge: %s)",
160+
self.versions.latest_version,
161+
self.minimum_spec_version,
162+
)
163+
sys.exit(18)
164+
152165
async def wait_for_connection(self) -> None:
153166
self.log.info("Ensuring connectivity to homeserver")
154167
errors = 0
155168
tried_to_register = False
156169
while True:
157170
try:
158171
self.versions = await self.az.intent.versions()
172+
await self.check_versions()
159173
await self.az.intent.whoami()
160174
break
161175
except (MUnknownToken, MExclusive):

mautrix/client/api/base.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ async def versions(self, no_cache: bool = False) -> VersionsResponse:
116116
"""
117117
if no_cache or not self.versions_cache:
118118
resp = await self.api.request(Method.GET, Path.versions)
119-
vers = self.versions_cache = VersionsResponse.deserialize(resp)
120-
if not vers.has_modern_versions and vers.has_legacy_versions:
121-
self.log.warning("Server isn't advertising modern spec versions")
119+
self.versions_cache = VersionsResponse.deserialize(resp)
122120
return self.versions_cache
123121

124122
@classmethod

mautrix/types/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@
144144
RoomCreatePreset,
145145
RoomDirectoryResponse,
146146
RoomDirectoryVisibility,
147-
VersionsResponse,
148147
)
149148
from .primitive import (
150149
JSON,
@@ -186,6 +185,7 @@
186185
field,
187186
serializer,
188187
)
188+
from .versions import SpecVersions, Version, VersionFormat, VersionsResponse
189189

190190
__all__ = [
191191
"DiscoveryInformation",
@@ -338,7 +338,6 @@
338338
"RoomCreatePreset",
339339
"RoomDirectoryResponse",
340340
"RoomDirectoryVisibility",
341-
"VersionsResponse",
342341
"JSON",
343342
"BatchID",
344343
"ContentURI",
@@ -375,4 +374,8 @@
375374
"deserializer",
376375
"field",
377376
"serializer",
377+
"SpecVersions",
378+
"Version",
379+
"VersionFormat",
380+
"VersionsResponse",
378381
]

mautrix/types/misc.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# This Source Code Form is subject to the terms of the Mozilla Public
44
# License, v. 2.0. If a copy of the MPL was not distributed with this
55
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6-
from typing import Dict, List, NamedTuple, NewType, Optional
6+
from typing import List, NamedTuple, NewType, Optional
77
from enum import Enum
88

99
from attr import dataclass
@@ -107,20 +107,6 @@ class RoomDirectoryResponse(SerializableAttrs):
107107
)
108108

109109

110-
@dataclass
111-
class VersionsResponse(SerializableAttrs):
112-
versions: List[str]
113-
unstable_features: Dict[str, bool] = attr.ib(factory=lambda: {})
114-
115-
@property
116-
def has_legacy_versions(self) -> bool:
117-
return any(v for v in self.versions if v.startswith("r0."))
118-
119-
@property
120-
def has_modern_versions(self) -> bool:
121-
return any(v for v in self.versions if v.startswith("v"))
122-
123-
124110
@dataclass
125111
class BatchSendResponse(SerializableAttrs):
126112
state_event_ids: List[EventID]

mautrix/types/versions.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright (c) 2022 Tulir Asokan
2+
#
3+
# This Source Code Form is subject to the terms of the Mozilla Public
4+
# License, v. 2.0. If a copy of the MPL was not distributed with this
5+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
from typing import Dict, List, NamedTuple, Optional, Union
7+
from enum import IntEnum
8+
import re
9+
10+
from attr import dataclass
11+
import attr
12+
13+
from . import JSON
14+
from .util import Serializable, SerializableAttrs
15+
16+
17+
class VersionFormat(IntEnum):
18+
UNKNOWN = -1
19+
LEGACY = 0
20+
MODERN = 1
21+
22+
def __repr__(self) -> str:
23+
return f"VersionFormat.{self.name}"
24+
25+
26+
legacy_version_regex = re.compile(r"^r(\d+)\.(\d+)\.(\d+)$")
27+
modern_version_regex = re.compile(r"^v(\d+)\.(\d+)$")
28+
29+
30+
@attr.dataclass(frozen=True)
31+
class Version(Serializable):
32+
format: VersionFormat
33+
major: int
34+
minor: int
35+
patch: int
36+
raw: str
37+
38+
def __str__(self) -> str:
39+
if self.format == VersionFormat.MODERN:
40+
return f"v{self.major}.{self.minor}"
41+
elif self.format == VersionFormat.LEGACY:
42+
return f"r{self.major}.{self.minor}.{self.patch}"
43+
else:
44+
return self.raw
45+
46+
def serialize(self) -> JSON:
47+
return str(self)
48+
49+
@classmethod
50+
def deserialize(cls, raw: JSON) -> "Version":
51+
assert isinstance(raw, str), "versions must be strings"
52+
if modern := modern_version_regex.fullmatch(raw):
53+
major, minor = modern.groups()
54+
return Version(VersionFormat.MODERN, int(major), int(minor), 0, raw)
55+
elif legacy := legacy_version_regex.fullmatch(raw):
56+
major, minor, patch = legacy.groups()
57+
return Version(VersionFormat.LEGACY, int(major), int(minor), int(patch), raw)
58+
else:
59+
return Version(VersionFormat.UNKNOWN, 0, 0, 0, raw)
60+
61+
62+
class SpecVersions:
63+
R010 = Version.deserialize("r0.1.0")
64+
R020 = Version.deserialize("r0.2.0")
65+
R030 = Version.deserialize("r0.3.0")
66+
R040 = Version.deserialize("r0.4.0")
67+
R050 = Version.deserialize("r0.5.0")
68+
R060 = Version.deserialize("r0.6.0")
69+
R061 = Version.deserialize("r0.6.1")
70+
V11 = Version.deserialize("v1.1")
71+
V12 = Version.deserialize("v1.2")
72+
V13 = Version.deserialize("v1.3")
73+
74+
75+
@dataclass
76+
class VersionsResponse(SerializableAttrs):
77+
versions: List[Version]
78+
unstable_features: Dict[str, bool] = attr.ib(factory=lambda: {})
79+
80+
def supports(self, thing: Union[Version, str]) -> Optional[bool]:
81+
"""
82+
Check if the versions response contains the given spec version or unstable feature.
83+
84+
Args:
85+
thing: The spec version (as a :class:`Version` or string)
86+
or unstable feature name (as a string) to check.
87+
88+
Returns:
89+
``True`` if the exact version or unstable feature is supported,
90+
``False`` if it's not supported,
91+
``None`` for unstable features which are not included in the response at all.
92+
"""
93+
if isinstance(thing, Version):
94+
return thing in self.versions
95+
elif (parsed_version := Version.deserialize(thing)).format != VersionFormat.UNKNOWN:
96+
return parsed_version in self.versions
97+
return self.unstable_features.get(thing)
98+
99+
def supports_at_least(self, version: Union[Version, str]) -> bool:
100+
"""
101+
Check if the versions response contains the given spec version or any higher version.
102+
103+
Args:
104+
version: The spec version as a :class:`Version` or a string.
105+
106+
Returns:
107+
``True`` if a version equal to or higher than the given version is found,
108+
``False`` otherwise.
109+
"""
110+
if isinstance(version, str):
111+
version = Version.deserialize(version)
112+
return any(v for v in self.versions if v > version)
113+
114+
@property
115+
def latest_version(self) -> Version:
116+
return max(self.versions)
117+
118+
@property
119+
def has_legacy_versions(self) -> bool:
120+
"""
121+
Check if the response contains any legacy (r0.x.y) versions.
122+
123+
.. deprecated:: 0.16.10
124+
:meth:`supports_at_least` and :meth:`supports` methods are now preferred.
125+
"""
126+
return any(v for v in self.versions if v.format == VersionFormat.LEGACY)
127+
128+
@property
129+
def has_modern_versions(self) -> bool:
130+
"""
131+
Check if the response contains any modern (v1.1 or higher) versions.
132+
133+
.. deprecated:: 0.16.10
134+
:meth:`supports_at_least` and :meth:`supports` methods are now preferred.
135+
"""
136+
return self.supports_at_least(SpecVersions.V11)

0 commit comments

Comments
 (0)