matrix-python 1.4.11a0__tar.gz → 1.5.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. matrix_python-1.5.0a0/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
  2. matrix_python-1.5.0a0/.github/ISSUE_TEMPLATE/feature--good-first.md +29 -0
  3. matrix_python-1.5.0a0/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  4. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/LICENSE +2 -2
  5. {matrix_python-1.4.11a0/matrix_python.egg-info → matrix_python-1.5.0a0}/PKG-INFO +10 -9
  6. matrix_python-1.5.0a0/docs/docs/reference/space.md +15 -0
  7. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/mkdocs.yml +2 -1
  8. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/__init__.py +2 -0
  9. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/_version.py +3 -3
  10. matrix_python-1.5.0a0/matrix/api.py +35 -0
  11. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/bot.py +68 -3
  12. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/message.py +24 -17
  13. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/room.py +91 -40
  14. matrix_python-1.5.0a0/matrix/space.py +45 -0
  15. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0/matrix_python.egg-info}/PKG-INFO +10 -9
  16. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix_python.egg-info/SOURCES.txt +10 -0
  17. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix_python.egg-info/requires.txt +7 -7
  18. matrix_python-1.5.0a0/matrix_python.egg-info/scm_file_list.json +105 -0
  19. matrix_python-1.5.0a0/matrix_python.egg-info/scm_version.json +8 -0
  20. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/pyproject.toml +7 -7
  21. matrix_python-1.5.0a0/tests/test_api.py +43 -0
  22. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_bot.py +98 -2
  23. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_message.py +53 -2
  24. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_room.py +183 -2
  25. matrix_python-1.5.0a0/tests/test_space.py +151 -0
  26. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.github/dependabot.yml +0 -0
  27. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.github/workflows/CODEOWNERS +0 -0
  28. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.github/workflows/codeql.yml +0 -0
  29. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.github/workflows/docs.yml +0 -0
  30. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.github/workflows/publish.yml +0 -0
  31. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.github/workflows/scorecard.yml +0 -0
  32. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.github/workflows/tests.yml +0 -0
  33. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/.gitignore +0 -0
  34. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/CODE_OF_CONDUCT.md +0 -0
  35. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/CONTRIBUTING.md +0 -0
  36. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/README.md +0 -0
  37. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/checks.md +0 -0
  38. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/cooldown.md +0 -0
  39. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/error-handling.md +0 -0
  40. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/extension.md +0 -0
  41. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/index.md +0 -0
  42. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/ping.md +0 -0
  43. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/reaction.md +0 -0
  44. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/examples/scheduler.md +0 -0
  45. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/bigger-bot.md +0 -0
  46. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/checks.md +0 -0
  47. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/commands.md +0 -0
  48. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/configuration.md +0 -0
  49. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/error-handling.md +0 -0
  50. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/events.md +0 -0
  51. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/groups.md +0 -0
  52. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/index.md +0 -0
  53. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/guides/introduction.md +0 -0
  54. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/img/favicon.svg +0 -0
  55. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/img/matrixpy-black.svg +0 -0
  56. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/img/matrixpy-white.svg +0 -0
  57. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/index.md +0 -0
  58. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/bot.md +0 -0
  59. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/checks.md +0 -0
  60. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/command.md +0 -0
  61. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/config.md +0 -0
  62. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/content.md +0 -0
  63. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/context.md +0 -0
  64. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/errors.md +0 -0
  65. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/extension.md +0 -0
  66. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/group.md +0 -0
  67. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/message.md +0 -0
  68. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/protocols.md +0 -0
  69. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/registry.md +0 -0
  70. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/room.md +0 -0
  71. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/scheduler.md +0 -0
  72. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/reference/types.md +0 -0
  73. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/docs/docs/stylesheets/extra.css +0 -0
  74. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/README.md +0 -0
  75. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/checks.py +0 -0
  76. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/config.yaml +0 -0
  77. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/cooldown.py +0 -0
  78. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/error_handling.py +0 -0
  79. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/extension.py +0 -0
  80. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/ping.py +0 -0
  81. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/reaction.py +0 -0
  82. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/examples/scheduler.py +0 -0
  83. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/checks.py +0 -0
  84. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/command.py +0 -0
  85. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/config.py +0 -0
  86. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/content.py +0 -0
  87. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/context.py +0 -0
  88. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/errors.py +0 -0
  89. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/extension.py +0 -0
  90. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/group.py +0 -0
  91. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/help/__init__.py +0 -0
  92. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/help/help_command.py +0 -0
  93. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/help/pagination.py +0 -0
  94. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/protocols.py +0 -0
  95. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/py.typed +0 -0
  96. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/registry.py +0 -0
  97. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/scheduler.py +0 -0
  98. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix/types.py +0 -0
  99. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix_python.egg-info/dependency_links.txt +0 -0
  100. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/matrix_python.egg-info/top_level.txt +0 -0
  101. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/mypy.ini +0 -0
  102. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/setup.cfg +0 -0
  103. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/help/test_default_help_command.py +0 -0
  104. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/help/test_help_command.py +0 -0
  105. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/help/test_pagination.py +0 -0
  106. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_command.py +0 -0
  107. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_config.py +0 -0
  108. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_context.py +0 -0
  109. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_extension.py +0 -0
  110. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_group.py +0 -0
  111. {matrix_python-1.4.11a0 → matrix_python-1.5.0a0}/tests/test_registry.py +0 -0
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: PenguinBoi12
7
+ type: Bug
8
+
9
+ ---
10
+
11
+ **Describe the bug**
12
+ A clear and concise description of what the bug is.
13
+
14
+ **To Reproduce**
15
+ Steps to reproduce the behavior:
16
+ 1. Go to '...'
17
+ 2. Click on '....'
18
+ 3. Scroll down to '....'
19
+ 4. See error
20
+
21
+ **Expected behavior**
22
+ A clear and concise description of what you expected to happen.
23
+
24
+ **Screenshots**
25
+ If applicable, add screenshots to help explain your problem.
26
+
27
+ **Additional context**
28
+ Add any other context about the problem here.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: 'Feature: Good First'
3
+ about: 'This template is for good first issues '
4
+ title: ''
5
+ labels: good first issue
6
+ assignees: ''
7
+ type: Feature
8
+
9
+ ---
10
+
11
+ Explain clearly what we want to add and why it's useful to add it. Give context when necessary.
12
+
13
+ ### Proposed API
14
+ This is a proposed API. It does not necessarily mean it's the final API but it's a good place to start.
15
+
16
+ ```python
17
+ Some code here
18
+ ```
19
+
20
+ **Example**
21
+ ```python
22
+ An example of usage of the feature
23
+ ```
24
+
25
+ ### Before opening a PR
26
+ - Write unit tests
27
+ - Run `mypy matrix/`
28
+ - Run `pytest tests/`
29
+ - Run `black .`
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+ type: Feature
8
+
9
+ ---
10
+
11
+ **Is your feature request related to a problem? Please describe.**
12
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13
+
14
+ **Describe the solution you'd like**
15
+ A clear and concise description of what you want to happen.
16
+
17
+ **Describe alternatives you've considered**
18
+ A clear and concise description of any alternative solutions or features you've considered.
19
+
20
+ **Additional context**
21
+ Add any other context or screenshots about the feature request here.
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Sebastián Ramírez
3
+ Copyright (c) 2025 Code Society Lab
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
21
+ THE SOFTWARE.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 1.4.11a0
3
+ Version: 1.5.0a0
4
4
  Summary: An easy-to-use Matrix bot framework designed for quick development and minimal setup
5
5
  Author: Simon Roy, Chris Dedman Rollet
6
6
  Maintainer-email: Code Society Lab <admin@codesociety.xyz>
7
7
  License: The MIT License (MIT)
8
8
 
9
- Copyright (c) 2021 Sebastián Ramírez
9
+ Copyright (c) 2025 Code Society Lab
10
10
 
11
11
  Permission is hereby granted, free of charge, to any person obtaining a copy
12
12
  of this software and associated documentation files (the "Software"), to deal
@@ -25,6 +25,7 @@ License: The MIT License (MIT)
25
25
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
26
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
27
  THE SOFTWARE.
28
+
28
29
  Project-URL: Homepage, https://codesociety.xyz
29
30
  Project-URL: Source, https://github.com/Code-Society-Lab/matrixpy
30
31
  Project-URL: Issues, https://github.com/Code-Society-Lab/matrixpy/issues
@@ -34,15 +35,15 @@ Requires-Dist: matrix-nio==0.25.2
34
35
  Requires-Dist: logger
35
36
  Requires-Dist: PyYAML==6.0.3
36
37
  Requires-Dist: markdown==3.10.2
37
- Requires-Dist: APScheduler==3.11.2
38
+ Requires-Dist: APScheduler==3.11.3
38
39
  Requires-Dist: envyaml==1.10.211231
39
40
  Provides-Extra: dev
40
- Requires-Dist: pytest==9.0.3; extra == "dev"
41
- Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
42
- Requires-Dist: black==26.3.1; extra == "dev"
43
- Requires-Dist: mypy==1.20.1; extra == "dev"
44
- Requires-Dist: types-PyYAML==6.0.12.20260408; extra == "dev"
45
- Requires-Dist: types-Markdown==3.10.2.20260408; extra == "dev"
41
+ Requires-Dist: pytest==9.1.1; extra == "dev"
42
+ Requires-Dist: pytest-asyncio==1.4.0; extra == "dev"
43
+ Requires-Dist: black==26.5.1; extra == "dev"
44
+ Requires-Dist: mypy==2.1.0; extra == "dev"
45
+ Requires-Dist: types-PyYAML==6.0.12.20260518; extra == "dev"
46
+ Requires-Dist: types-Markdown==3.10.2.20260518; extra == "dev"
46
47
  Provides-Extra: doc
47
48
  Requires-Dist: mkdocs==1.6.1; extra == "doc"
48
49
  Requires-Dist: mkdocs-material==9.7.6; extra == "doc"
@@ -0,0 +1,15 @@
1
+ # Space
2
+
3
+ `Space` extends `Room` to represent a Matrix Space. It is returned by `Bot.get_space()` and `Bot.get_spaces()` instead of a plain `Room` whenever the room type is `m.space`.
4
+
5
+ ```python
6
+ from matrix import Bot
7
+
8
+ bot = Bot()
9
+
10
+ space = bot.get_space("!space123:matrix.org")
11
+ if space:
12
+ print(space.name)
13
+ ```
14
+
15
+ ::: matrix.space.Space
@@ -1,7 +1,7 @@
1
1
  site_name: matrix.py
2
2
  site_description: A simple, developer-friendly library to create powerful Matrix bots.
3
3
  site_url: https://matrixpy.codesociety.xyz/
4
- repo_url: https://github.com/Code-Society-LabHow t/matrixpy
4
+ repo_url: https://github.com/Code-Society-Lab/matrixpy
5
5
  repo_name: Code-Society-Lab/matrixpy
6
6
  theme:
7
7
  name: material
@@ -109,6 +109,7 @@ nav:
109
109
  - Protocols: reference/protocols.md
110
110
  - Registry: reference/registry.md
111
111
  - Room: reference/room.md
112
+ - Space: reference/space.md
112
113
  - Scheduler: reference/scheduler.md
113
114
  - Types: reference/types.md
114
115
  - Examples:
@@ -15,6 +15,7 @@ from .command import Command
15
15
  from .help import HelpCommand
16
16
  from .checks import cooldown
17
17
  from .room import Room
18
+ from .space import Space
18
19
  from .message import Message
19
20
  from .extension import Extension
20
21
 
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "HelpCommand",
29
30
  "cooldown",
30
31
  "Room",
32
+ "Space",
31
33
  "Message",
32
34
  "Extension",
33
35
  ]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.4.11a0'
22
- __version_tuple__ = version_tuple = (1, 4, 11, 'a0')
21
+ __version__ = version = '1.5.0a0'
22
+ __version_tuple__ = version_tuple = (1, 5, 0, 'a0')
23
23
 
24
- __commit_id__ = commit_id = 'gde6b74fcb'
24
+ __commit_id__ = commit_id = 'g91a8b536b'
@@ -0,0 +1,35 @@
1
+ from typing import Awaitable, TypeVar
2
+
3
+ from nio import ErrorResponse, Response
4
+
5
+ from matrix.errors import MatrixError
6
+
7
+ T = TypeVar("T", bound=Response)
8
+
9
+
10
+ async def matrix_call(coro: Awaitable[T], /, *, error_message: str) -> T:
11
+ """Await `coro`, translating any failure into a `MatrixError`.
12
+
13
+ matrix-nio's `AsyncClient` methods don't raise on API-level errors; they
14
+ return an `ErrorResponse` instead of raising. This wraps a single call so
15
+ both transport-level exceptions and nio `ErrorResponse` results become a
16
+ `MatrixError` carrying `error_message`.
17
+
18
+ ## Example
19
+
20
+ ```python
21
+ response = await matrix_call(
22
+ self.client.room_kick(room_id=self.room_id, user_id=user_id),
23
+ error_message="Failed to kick user",
24
+ )
25
+ ```
26
+ """
27
+ try:
28
+ response = await coro
29
+ except Exception as e:
30
+ raise MatrixError(f"{error_message}: {e}") from e
31
+
32
+ if isinstance(response, ErrorResponse):
33
+ raise MatrixError(f"{error_message}: {response}")
34
+
35
+ return response
@@ -7,7 +7,8 @@ from typing import Optional, Any
7
7
 
8
8
  from nio import AsyncClient, Event, MatrixRoom
9
9
 
10
- from .room import Room
10
+ from .room import Room, make_room
11
+ from .space import Space
11
12
  from .group import Group
12
13
  from .config import Config
13
14
  from .context import Context
@@ -21,6 +22,7 @@ from .errors import (
21
22
  CheckError,
22
23
  RoomNotFoundError,
23
24
  )
25
+ from .api import matrix_call
24
26
 
25
27
 
26
28
  class Bot(Registry):
@@ -87,19 +89,79 @@ class Bot(Registry):
87
89
 
88
90
  Returns the `Room` object corresponding to `room_id` if it exists in
89
91
  the client's known rooms. Returns `None` if the room cannot be found.
92
+ Returns a typed subclass if the room type is registered (e.g. `Space` for m.space rooms).
90
93
 
91
94
  ## Example
92
95
 
93
96
  ```python
94
97
  room = bot.get_room("!abc123:matrix.org")
98
+
95
99
  if room:
96
100
  print(room.name)
97
101
  ```
98
102
  """
99
103
  if matrix_room := self.client.rooms.get(room_id):
100
- return Room(matrix_room=matrix_room, client=self.client)
104
+ return make_room(matrix_room, self.client)
101
105
  return None
102
106
 
107
+ def get_rooms(self) -> list[Room]:
108
+ """Retrieve a list of all rooms the bot is aware of.
109
+
110
+ This method returns a list of `Room` objects for all rooms currently
111
+ known to the client. This includes both regular rooms and spaces;
112
+ spaces are returned as `Space` instances.
113
+
114
+ ## Example
115
+
116
+ ```python
117
+ rooms = bot.get_rooms()
118
+
119
+ for room in rooms:
120
+ print(room.name)
121
+ ```
122
+ """
123
+ rooms = []
124
+
125
+ for matrix_room in self.client.rooms.values():
126
+ rooms.append(make_room(matrix_room, self.client))
127
+
128
+ return rooms
129
+
130
+ def get_space(self, space_id: str) -> Space | None:
131
+ """Retrieve a `Space` instance by its Matrix room ID.
132
+
133
+ Returns the `Space` object corresponding to `space_id` if it exists in
134
+ the client's known rooms and is a space. Returns `None` otherwise.
135
+
136
+ ## Example
137
+
138
+ ```python
139
+ space = bot.get_space("!space123:matrix.org")
140
+
141
+ if space:
142
+ print(space.name)
143
+ ```
144
+ """
145
+ room = self.get_room(space_id)
146
+ return room if isinstance(room, Space) else None
147
+
148
+ def get_spaces(self) -> list[Space]:
149
+ """Retrieve a list of all spaces the bot is aware of.
150
+
151
+ This method returns a list of `Space` objects for all rooms currently
152
+ known to the client that are identified as spaces.
153
+
154
+ ## Example
155
+
156
+ ```python
157
+ spaces = bot.get_spaces()
158
+
159
+ for space in spaces:
160
+ print(space.name)
161
+ ```
162
+ """
163
+ return [room for room in self.get_rooms() if isinstance(room, Space)]
164
+
103
165
  def load_extension(self, extension: Extension) -> None:
104
166
  self.log.debug(f"Loading extension: '{extension.name}'")
105
167
 
@@ -267,7 +329,10 @@ class Bot(Registry):
267
329
  if self.config.token:
268
330
  self.client.access_token = self.config.token
269
331
  else:
270
- login_resp = await self.client.login(self.config.password)
332
+ login_resp = await matrix_call(
333
+ self.client.login(self.config.password),
334
+ error_message="Failed to log in",
335
+ )
271
336
  self.log.info("logged in: %s", login_resp)
272
337
 
273
338
  sync_task = asyncio.create_task(self.client.sync_forever(timeout=30_000))
@@ -5,6 +5,7 @@ from nio import AsyncClient, Event
5
5
  from matrix.types import Reaction
6
6
  from matrix.content import ReactionContent, EditContent
7
7
  from matrix.errors import MatrixError
8
+ from matrix.api import matrix_call
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from .room import Room # pragma: no cover
@@ -116,14 +117,14 @@ class Message:
116
117
  """
117
118
  content = ReactionContent(event_id=self.event_id, emoji=emoji)
118
119
 
119
- try:
120
- await self.client.room_send(
120
+ await matrix_call(
121
+ self.client.room_send(
121
122
  room_id=self.room.room_id,
122
123
  message_type="m.reaction",
123
124
  content=content.build(),
124
- )
125
- except Exception as e:
126
- raise MatrixError(f"Failed to add reaction: {e}")
125
+ ),
126
+ error_message="Failed to add reaction",
127
+ )
127
128
 
128
129
  async def edit(self, new_body: str) -> None:
129
130
  """Updates the message content to the new text.
@@ -139,19 +140,21 @@ class Message:
139
140
  """
140
141
  content = EditContent(new_body, original_event_id=self.event_id)
141
142
 
142
- try:
143
- await self.client.room_send(
143
+ await matrix_call(
144
+ self.client.room_send(
144
145
  room_id=self.room.room_id,
145
146
  message_type="m.room.message",
146
147
  content=content.build(),
147
- )
148
- self._body = new_body
149
- except Exception as e:
150
- raise MatrixError(f"Failed to edit message: {e}")
148
+ ),
149
+ error_message="Failed to edit message",
150
+ )
151
+ self._body = new_body
151
152
 
152
- async def delete(self) -> None:
153
+ async def delete(self, reason: str | None = None) -> None:
153
154
  """Removes the message content from the room. This action cannot be undone.
154
155
 
156
+ Optionally provide a reason that will be visible to room moderators.
157
+
155
158
  ## Example
156
159
 
157
160
  ```python
@@ -159,12 +162,16 @@ class Message:
159
162
  async def oops(ctx: Context):
160
163
  msg = await ctx.reply("Secret info!")
161
164
  await msg.delete()
165
+
166
+ # Delete with a reason
167
+ await message.delete(reason="Violated room rules")
162
168
  ```
163
169
  """
164
- try:
165
- await self.client.room_redact(
170
+ await matrix_call(
171
+ self.client.room_redact(
166
172
  room_id=self.room.room_id,
167
173
  event_id=self.event_id,
168
- )
169
- except Exception as e:
170
- raise MatrixError(f"Failed to delete message: {e}")
174
+ reason=reason,
175
+ ),
176
+ error_message="Failed to delete message",
177
+ )
@@ -2,7 +2,7 @@ from typing import Any
2
2
 
3
3
  from nio import AsyncClient, MatrixRoom, Event
4
4
 
5
- from matrix.errors import MatrixError
5
+ from matrix.api import matrix_call
6
6
  from matrix.message import Message
7
7
  from matrix.content import (
8
8
  BaseMessageContent,
@@ -17,10 +17,26 @@ from matrix.content import (
17
17
  )
18
18
  from matrix.types import File, Image, Audio, Video
19
19
 
20
+ _registry: dict[str, type["Room"]] = {}
21
+
22
+
23
+ def make_room(matrix_room: MatrixRoom, client: AsyncClient) -> "Room":
24
+ room_cls = _registry.get(str(matrix_room.room_type), Room)
25
+ return room_cls(matrix_room, client)
26
+
20
27
 
21
28
  class Room:
22
29
  """Represents a Matrix room and provides methods to interact with it."""
23
30
 
31
+ def __init_subclass__(cls, room_type: str | None = None, **kwargs: Any) -> None:
32
+ super().__init_subclass__(**kwargs)
33
+ if room_type:
34
+ if room_type in _registry:
35
+ raise ValueError(
36
+ f"Room type '{room_type}' is already registered by {_registry[room_type].__name__}"
37
+ )
38
+ _registry[room_type] = cls
39
+
24
40
  def __init__(self, matrix_room: MatrixRoom, client: AsyncClient) -> None:
25
41
  self._matrix_room: MatrixRoom = matrix_room
26
42
  self._client: AsyncClient = client
@@ -313,21 +329,21 @@ class Room:
313
329
 
314
330
  async def _send_payload(self, payload: BaseMessageContent) -> Message:
315
331
  """Send a BaseMessageContent payload and return a Message object."""
316
- try:
317
- resp = await self.client.room_send(
332
+ resp = await matrix_call(
333
+ self.client.room_send(
318
334
  room_id=self.room_id,
319
335
  message_type="m.room.message",
320
336
  content=payload.build(),
321
- )
322
- event = await self.fetch_event(resp.event_id)
337
+ ),
338
+ error_message="Failed to send message",
339
+ )
340
+ event = await self.fetch_event(resp.event_id)
323
341
 
324
- return Message(
325
- room=self,
326
- event=event,
327
- client=self.client,
328
- )
329
- except Exception as e:
330
- raise MatrixError(f"Failed to send message: {e}")
342
+ return Message(
343
+ room=self,
344
+ event=event,
345
+ client=self.client,
346
+ )
331
347
 
332
348
  async def fetch_event(self, event_id: str) -> Event:
333
349
  """Fetch a Matrix event by its ID.
@@ -338,14 +354,11 @@ class Room:
338
354
  print(event.sender)
339
355
  ```
340
356
  """
341
- try:
342
- response = await self.client.room_get_event(
343
- room_id=self.room_id,
344
- event_id=event_id,
345
- )
346
- return response.event
347
- except Exception as e:
348
- raise MatrixError(f"Failed to get event: {e}")
357
+ response = await matrix_call(
358
+ self.client.room_get_event(room_id=self.room_id, event_id=event_id),
359
+ error_message="Failed to get event",
360
+ )
361
+ return response.event
349
362
 
350
363
  async def fetch_message(self, event_id: str) -> Message:
351
364
  """Fetch a Message by its event ID.
@@ -363,6 +376,29 @@ class Room:
363
376
  client=self.client,
364
377
  )
365
378
 
379
+ async def mark_as_read(self, event_id: str) -> None:
380
+ """Send a read receipt for the given event.
381
+
382
+ Signals to other clients that the bot has read up to this event. Useful
383
+ for bots that process messages silently without sending a reply.
384
+
385
+ ## Example
386
+
387
+ ```python
388
+ @bot.event
389
+ async def on_message(room: Room, event: Event):
390
+ await room.mark_as_read(event.event_id)
391
+ ```
392
+ """
393
+ await matrix_call(
394
+ self.client.room_read_markers(
395
+ room_id=self.room_id,
396
+ fully_read_event=event_id,
397
+ read_event=event_id,
398
+ ),
399
+ error_message="Failed to mark as read",
400
+ )
401
+
366
402
  async def invite_user(self, user_id: str) -> None:
367
403
  """Invite a user to the room.
368
404
 
@@ -376,10 +412,10 @@ class Room:
376
412
  await room.invite_user("@alice:example.com")
377
413
  ```
378
414
  """
379
- try:
380
- await self.client.room_invite(room_id=self.room_id, user_id=user_id)
381
- except Exception as e:
382
- raise MatrixError(f"Failed to invite user: {e}")
415
+ await matrix_call(
416
+ self.client.room_invite(room_id=self.room_id, user_id=user_id),
417
+ error_message="Failed to invite user",
418
+ )
383
419
 
384
420
  async def ban_user(self, user_id: str, reason: str | None = None) -> None:
385
421
  """Ban a user from the room.
@@ -397,12 +433,10 @@ class Room:
397
433
  await room.ban_user("@spammer:example.com", reason="Spam and harassment")
398
434
  ```
399
435
  """
400
- try:
401
- await self.client.room_ban(
402
- room_id=self.room_id, user_id=user_id, reason=reason
403
- )
404
- except Exception as e:
405
- raise MatrixError(f"Failed to ban user: {e}")
436
+ await matrix_call(
437
+ self.client.room_ban(room_id=self.room_id, user_id=user_id, reason=reason),
438
+ error_message="Failed to ban user",
439
+ )
406
440
 
407
441
  async def unban_user(self, user_id: str) -> None:
408
442
  """Unban a user from the room.
@@ -417,10 +451,10 @@ class Room:
417
451
  await room.unban_user("@alice:example.com")
418
452
  ```
419
453
  """
420
- try:
421
- await self.client.room_unban(room_id=self.room_id, user_id=user_id)
422
- except Exception as e:
423
- raise MatrixError(f"Failed to unban user: {e}")
454
+ await matrix_call(
455
+ self.client.room_unban(room_id=self.room_id, user_id=user_id),
456
+ error_message="Failed to unban user",
457
+ )
424
458
 
425
459
  async def kick_user(self, user_id: str, reason: str | None = None) -> None:
426
460
  """Kick a user from the room.
@@ -439,9 +473,26 @@ class Room:
439
473
  await room.kick_user("@troublemaker:example.com", reason="Violating room rules")
440
474
  ```
441
475
  """
442
- try:
443
- await self.client.room_kick(
444
- room_id=self.room_id, user_id=user_id, reason=reason
445
- )
446
- except Exception as e:
447
- raise MatrixError(f"Failed to kick user: {e}")
476
+ await matrix_call(
477
+ self.client.room_kick(room_id=self.room_id, user_id=user_id, reason=reason),
478
+ error_message="Failed to kick user",
479
+ )
480
+
481
+ async def get_members(self) -> list[str]:
482
+ """Fetch the list of user IDs currently joined to the room.
483
+
484
+ This queries the Matrix server directly for the current membership,
485
+ which may include members not yet reflected in local room state.
486
+
487
+ ## Example
488
+
489
+ ```python
490
+ members = await room.get_members()
491
+ print(f"{len(members)} members: {', '.join(members)}")
492
+ ```
493
+ """
494
+ response = await matrix_call(
495
+ self.client.joined_members(self.room_id),
496
+ error_message="Failed to get members",
497
+ )
498
+ return [member.user_id for member in response.members]
@@ -0,0 +1,45 @@
1
+ from typing import Self
2
+ from matrix.room import Room, make_room
3
+
4
+
5
+ class Space(Room, room_type="m.space"):
6
+ def get_children(self, depth: int = 1) -> list[Room | Self]:
7
+ """Return the child rooms and spaces of this space that the bot has joined.
8
+
9
+ Children the bot has not joined are silently omitted. Use `depth` to
10
+ recursively collect children of sub-spaces. `depth=1` returns direct
11
+ children only (default).
12
+
13
+ ## Example
14
+
15
+ ```python
16
+ space = bot.get_space("!space123:matrix.org")
17
+
18
+ for child in space.get_children():
19
+ print(child.name)
20
+
21
+ for child in space.get_children(depth=3):
22
+ print(child.name)
23
+ ```
24
+ """
25
+ children: list[Room | Self] = []
26
+
27
+ if depth < 0:
28
+ raise ValueError(f"depth must be a non-negative integer, got {depth}")
29
+
30
+ if depth == 0:
31
+ return []
32
+
33
+ for room_id in self.children:
34
+ matrix_room = self._client.rooms.get(room_id)
35
+
36
+ if not matrix_room:
37
+ continue
38
+
39
+ child = make_room(matrix_room, self._client)
40
+ children.append(child)
41
+
42
+ if isinstance(child, Space) and depth > 1:
43
+ children.extend(child.get_children(depth - 1))
44
+
45
+ return children