matrix-python 1.0.4a0__tar.gz → 1.1.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.
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/.github/workflows/publish.yml +2 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/.gitignore +2 -1
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/PKG-INFO +1 -1
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/__init__.py +6 -1
- matrix_python-1.1.0a0/matrix/_version.py +34 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/bot.py +11 -15
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/command.py +42 -11
- matrix_python-1.1.0a0/matrix/content.py +174 -0
- matrix_python-1.1.0a0/matrix/context.py +142 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/group.py +1 -1
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/help/help_command.py +24 -18
- matrix_python-1.1.0a0/matrix/message.py +131 -0
- matrix_python-1.1.0a0/matrix/room.py +413 -0
- matrix_python-1.1.0a0/matrix/types.py +26 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix_python.egg-info/PKG-INFO +1 -1
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix_python.egg-info/SOURCES.txt +3 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/pyproject.toml +4 -1
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/test_command.py +103 -0
- matrix_python-1.1.0a0/tests/test_context.py +225 -0
- matrix_python-1.1.0a0/tests/test_message.py +139 -0
- matrix_python-1.1.0a0/tests/test_room.py +356 -0
- matrix_python-1.0.4a0/matrix/context.py +0 -96
- matrix_python-1.0.4a0/matrix/message.py +0 -134
- matrix_python-1.0.4a0/matrix/room.py +0 -119
- matrix_python-1.0.4a0/tests/test_context.py +0 -96
- matrix_python-1.0.4a0/tests/test_message.py +0 -91
- matrix_python-1.0.4a0/tests/test_room.py +0 -151
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/.github/workflows/codeql.yml +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/.github/workflows/scorecard.yml +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/.github/workflows/tests.yml +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/CODE_OF_CONDUCT.md +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/CONTRIBUTING.md +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/LICENSE +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/README.md +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/README.md +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/checks.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/config.yaml +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/cooldown.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/error_handling.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/ping.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/reaction.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/examples/scheduler.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/checks.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/config.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/errors.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/help/__init__.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/help/pagination.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix/scheduler.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix_python.egg-info/dependency_links.txt +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix_python.egg-info/requires.txt +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/matrix_python.egg-info/top_level.txt +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/mypy.ini +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/setup.cfg +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/config_fixture.yaml +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/config_fixture_token.yaml +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/help/test_default_help_command.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/help/test_help_command.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/help/test_pagination.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/test_bot.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/test_config.py +0 -0
- {matrix_python-1.0.4a0 → matrix_python-1.1.0a0}/tests/test_group.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrix-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""A simple, developer-friendly library to create powerful Matrix bots."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("matrix-python")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
from matrix._version import version as __version__
|
|
4
9
|
|
|
5
10
|
from .bot import Bot
|
|
6
11
|
from .group import Group
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.1.0a0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 1, 0, 'a0')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'gcd36e40dc'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import inspect
|
|
2
3
|
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
|
|
@@ -113,7 +114,7 @@ class Bot:
|
|
|
113
114
|
|
|
114
115
|
:raises TypeError: If the function is not a coroutine.
|
|
115
116
|
"""
|
|
116
|
-
if not
|
|
117
|
+
if not inspect.iscoroutinefunction(func):
|
|
117
118
|
raise TypeError("Checks must be coroutine")
|
|
118
119
|
|
|
119
120
|
self.checks.append(func)
|
|
@@ -161,7 +162,7 @@ class Bot:
|
|
|
161
162
|
"""
|
|
162
163
|
|
|
163
164
|
def wrapper(f: Callback) -> Callback:
|
|
164
|
-
if not
|
|
165
|
+
if not inspect.iscoroutinefunction(f):
|
|
165
166
|
raise TypeError("Event handlers must be coroutines")
|
|
166
167
|
|
|
167
168
|
if event_spec:
|
|
@@ -285,7 +286,7 @@ class Bot:
|
|
|
285
286
|
"""
|
|
286
287
|
|
|
287
288
|
def wrapper(f: Callback) -> Callback:
|
|
288
|
-
if not
|
|
289
|
+
if not inspect.iscoroutinefunction(f):
|
|
289
290
|
raise TypeError("Scheduled tasks must be coroutines")
|
|
290
291
|
|
|
291
292
|
self.scheduler.schedule(cron, f)
|
|
@@ -324,7 +325,7 @@ class Bot:
|
|
|
324
325
|
"""
|
|
325
326
|
|
|
326
327
|
def wrapper(func: ErrorCallback) -> Callable:
|
|
327
|
-
if not
|
|
328
|
+
if not inspect.iscoroutinefunction(func):
|
|
328
329
|
raise TypeError("The error handler must be a coroutine.")
|
|
329
330
|
|
|
330
331
|
if exception:
|
|
@@ -336,22 +337,16 @@ class Bot:
|
|
|
336
337
|
return wrapper
|
|
337
338
|
|
|
338
339
|
def get_room(self, room_id: str) -> Room:
|
|
339
|
-
"""
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
:param room_id: The ID of the room to retrieve.
|
|
343
|
-
:type room_id: str
|
|
344
|
-
:return: An instance of the Room class.
|
|
345
|
-
:rtype: Room
|
|
346
|
-
"""
|
|
347
|
-
return Room(room_id=room_id, bot=self)
|
|
340
|
+
"""Retrieve a Room instance based on the room_id."""
|
|
341
|
+
matrix_room = self.client.rooms[room_id]
|
|
342
|
+
return Room(matrix_room=matrix_room, client=self.client)
|
|
348
343
|
|
|
349
344
|
def _auto_register_events(self) -> None:
|
|
350
345
|
for attr in dir(self):
|
|
351
346
|
if not attr.startswith("on_"):
|
|
352
347
|
continue
|
|
353
348
|
coro = getattr(self, attr, None)
|
|
354
|
-
if
|
|
349
|
+
if inspect.iscoroutinefunction(coro):
|
|
355
350
|
try:
|
|
356
351
|
self.event(coro)
|
|
357
352
|
except ValueError: # ignore unknown name
|
|
@@ -389,8 +384,9 @@ class Bot:
|
|
|
389
384
|
|
|
390
385
|
await ctx.command(ctx)
|
|
391
386
|
|
|
392
|
-
async def _build_context(self,
|
|
387
|
+
async def _build_context(self, matrix_room: MatrixRoom, event: Event) -> Context:
|
|
393
388
|
"""Builds the base context and extracts the command from the event"""
|
|
389
|
+
room = self.get_room(matrix_room.room_id)
|
|
394
390
|
ctx = Context(bot=self, room=room, event=event)
|
|
395
391
|
|
|
396
392
|
if not self.prefix or not ctx.body.startswith(self.prefix):
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import inspect
|
|
2
|
+
import types
|
|
3
3
|
|
|
4
4
|
from typing import (
|
|
5
5
|
TYPE_CHECKING,
|
|
6
6
|
Any,
|
|
7
|
+
Union,
|
|
7
8
|
Optional,
|
|
8
9
|
Callable,
|
|
9
10
|
Coroutine,
|
|
10
11
|
List,
|
|
11
12
|
get_type_hints,
|
|
12
13
|
DefaultDict,
|
|
14
|
+
get_args,
|
|
15
|
+
get_origin,
|
|
13
16
|
)
|
|
14
17
|
|
|
15
18
|
from .errors import MissingArgumentError, CheckError, CooldownError
|
|
@@ -98,7 +101,7 @@ class Command:
|
|
|
98
101
|
:type func: Callback
|
|
99
102
|
:raises TypeError: If the provided function is not a coroutine.
|
|
100
103
|
"""
|
|
101
|
-
if not
|
|
104
|
+
if not inspect.iscoroutinefunction(func):
|
|
102
105
|
raise TypeError("Commands must be coroutines")
|
|
103
106
|
|
|
104
107
|
self._callback = func
|
|
@@ -134,23 +137,51 @@ class Command:
|
|
|
134
137
|
return f"{self.prefix}{command_name} {params}"
|
|
135
138
|
|
|
136
139
|
def _parse_arguments(self, ctx: "Context") -> list[Any]:
|
|
140
|
+
args = ctx.args
|
|
137
141
|
parsed_args = []
|
|
138
142
|
|
|
139
143
|
for i, param in enumerate(self.params):
|
|
140
144
|
param_type = self.type_hints.get(param.name, str)
|
|
141
145
|
|
|
142
|
-
if i >= len(
|
|
146
|
+
if i >= len(args):
|
|
143
147
|
if param.default is not inspect.Parameter.empty:
|
|
144
148
|
parsed_args.append(param.default)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
continue
|
|
149
|
+
continue
|
|
150
|
+
raise MissingArgumentError(param)
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
if param.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
153
|
+
parsed_args.extend(
|
|
154
|
+
self._convert_type(param_type, arg) for arg in args[i:]
|
|
155
|
+
)
|
|
156
|
+
return parsed_args
|
|
157
|
+
|
|
158
|
+
converted_arg = self._convert_type(param_type, args[i])
|
|
150
159
|
parsed_args.append(converted_arg)
|
|
151
160
|
|
|
152
161
|
return parsed_args
|
|
153
162
|
|
|
163
|
+
def _convert_type(self, param_type: type, value: str) -> Any:
|
|
164
|
+
origin = get_origin(param_type)
|
|
165
|
+
|
|
166
|
+
if origin is Union or isinstance(param_type, types.UnionType):
|
|
167
|
+
union_types = get_args(param_type)
|
|
168
|
+
|
|
169
|
+
for union_type in union_types:
|
|
170
|
+
if union_type is type(None):
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
return union_type(value)
|
|
175
|
+
except (ValueError, TypeError):
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
return value
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
return param_type(value)
|
|
182
|
+
except (ValueError, TypeError):
|
|
183
|
+
return value
|
|
184
|
+
|
|
154
185
|
def check(self, func: Callback) -> None:
|
|
155
186
|
"""
|
|
156
187
|
Register a check callback
|
|
@@ -160,7 +191,7 @@ class Command:
|
|
|
160
191
|
|
|
161
192
|
:raises TypeError: If the function is not a coroutine.
|
|
162
193
|
"""
|
|
163
|
-
if not
|
|
194
|
+
if not inspect.iscoroutinefunction(func):
|
|
164
195
|
raise TypeError("Checks must be coroutine")
|
|
165
196
|
|
|
166
197
|
self.checks.append(func)
|
|
@@ -202,7 +233,7 @@ class Command:
|
|
|
202
233
|
:raises TypeError: If the function is not a coroutine.
|
|
203
234
|
"""
|
|
204
235
|
|
|
205
|
-
if not
|
|
236
|
+
if not inspect.iscoroutinefunction(func):
|
|
206
237
|
raise TypeError("The hook must be a coroutine.")
|
|
207
238
|
|
|
208
239
|
self._before_invoke_callback = func
|
|
@@ -217,7 +248,7 @@ class Command:
|
|
|
217
248
|
:raises TypeError: If the function is not a coroutine.
|
|
218
249
|
"""
|
|
219
250
|
|
|
220
|
-
if not
|
|
251
|
+
if not inspect.iscoroutinefunction(func):
|
|
221
252
|
raise TypeError("The hook must be a coroutine.")
|
|
222
253
|
|
|
223
254
|
self._after_invoke_callback = func
|
|
@@ -234,7 +265,7 @@ class Command:
|
|
|
234
265
|
"""
|
|
235
266
|
|
|
236
267
|
def wrapper(func: ErrorCallback) -> Callable:
|
|
237
|
-
if not
|
|
268
|
+
if not inspect.iscoroutinefunction(func):
|
|
238
269
|
raise TypeError("The error handler must be a coroutine.")
|
|
239
270
|
|
|
240
271
|
if exception:
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from markdown import markdown
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseMessageContent(ABC):
|
|
8
|
+
"""Base class for outgoing message payloads."""
|
|
9
|
+
|
|
10
|
+
msgtype: str
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def build(self) -> dict[str, Any]:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TextContent(BaseMessageContent):
|
|
19
|
+
msgtype = "m.text"
|
|
20
|
+
body: str
|
|
21
|
+
|
|
22
|
+
def build(self) -> dict:
|
|
23
|
+
return {"msgtype": self.msgtype, "body": self.body}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MarkdownMessage(TextContent):
|
|
28
|
+
def build(self) -> dict:
|
|
29
|
+
return {
|
|
30
|
+
"msgtype": self.msgtype,
|
|
31
|
+
"body": self.body,
|
|
32
|
+
"format": "org.matrix.custom.html",
|
|
33
|
+
"formatted_body": markdown(self.body, extensions=["nl2br"]),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class NoticeContent(TextContent):
|
|
39
|
+
msgtype = "m.notice"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ReplyContent(TextContent):
|
|
44
|
+
reply_to_event_id: str
|
|
45
|
+
|
|
46
|
+
def build(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"msgtype": self.msgtype,
|
|
49
|
+
"body": self.body,
|
|
50
|
+
"m.relates_to": {"m.in_reply_to": {"event_id": self.reply_to_event_id}},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class EditContent(TextContent):
|
|
56
|
+
original_event_id: str
|
|
57
|
+
|
|
58
|
+
def build(self) -> dict:
|
|
59
|
+
return {
|
|
60
|
+
"msgtype": self.msgtype,
|
|
61
|
+
"body": f"* {self.body}",
|
|
62
|
+
"m.new_content": {
|
|
63
|
+
"msgtype": "m.text",
|
|
64
|
+
"body": self.body,
|
|
65
|
+
},
|
|
66
|
+
"m.relates_to": {
|
|
67
|
+
"rel_type": "m.replace",
|
|
68
|
+
"event_id": self.original_event_id,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class FileContent(BaseMessageContent):
|
|
75
|
+
msgtype = "m.file"
|
|
76
|
+
filename: str
|
|
77
|
+
url: str
|
|
78
|
+
mimetype: str
|
|
79
|
+
|
|
80
|
+
def build(self) -> dict:
|
|
81
|
+
return {
|
|
82
|
+
"msgtype": self.msgtype,
|
|
83
|
+
"body": self.filename,
|
|
84
|
+
"url": self.url,
|
|
85
|
+
"info": {"mimetype": self.mimetype},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ImageContent(FileContent):
|
|
91
|
+
msgtype = "m.image"
|
|
92
|
+
height: int = 0
|
|
93
|
+
width: int = 0
|
|
94
|
+
|
|
95
|
+
def build(self) -> dict:
|
|
96
|
+
return {
|
|
97
|
+
"msgtype": self.msgtype,
|
|
98
|
+
"body": self.filename,
|
|
99
|
+
"url": self.url,
|
|
100
|
+
"info": {
|
|
101
|
+
"mimetype": self.mimetype,
|
|
102
|
+
"h": self.height,
|
|
103
|
+
"w": self.width,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class AudioContent(FileContent):
|
|
110
|
+
msgtype = "m.audio"
|
|
111
|
+
duration: int = 0
|
|
112
|
+
|
|
113
|
+
def build(self) -> dict:
|
|
114
|
+
return {
|
|
115
|
+
"msgtype": self.msgtype,
|
|
116
|
+
"body": self.filename,
|
|
117
|
+
"url": self.url,
|
|
118
|
+
"info": {
|
|
119
|
+
"mimetype": self.mimetype,
|
|
120
|
+
"duration": self.duration,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class VideoContent(FileContent):
|
|
127
|
+
msgtype = "m.video"
|
|
128
|
+
height: int = 0
|
|
129
|
+
width: int = 0
|
|
130
|
+
duration: int = 0
|
|
131
|
+
|
|
132
|
+
def build(self) -> dict:
|
|
133
|
+
return {
|
|
134
|
+
"msgtype": self.msgtype,
|
|
135
|
+
"body": self.filename,
|
|
136
|
+
"url": self.url,
|
|
137
|
+
"info": {
|
|
138
|
+
"mimetype": self.mimetype,
|
|
139
|
+
"h": self.height,
|
|
140
|
+
"w": self.width,
|
|
141
|
+
"duration": self.duration,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class LocationContent(BaseMessageContent):
|
|
148
|
+
msgtype = "m.location"
|
|
149
|
+
geo_uri: str
|
|
150
|
+
description: str = ""
|
|
151
|
+
|
|
152
|
+
def build(self) -> dict:
|
|
153
|
+
return {
|
|
154
|
+
"msgtype": self.msgtype,
|
|
155
|
+
"body": self.description or self.geo_uri,
|
|
156
|
+
"geo_uri": self.geo_uri,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class ReactionContent(BaseMessageContent):
|
|
162
|
+
"""For sending reactions to an event."""
|
|
163
|
+
|
|
164
|
+
event_id: str
|
|
165
|
+
emoji: str
|
|
166
|
+
|
|
167
|
+
def build(self) -> dict:
|
|
168
|
+
return {
|
|
169
|
+
"m.relates_to": {
|
|
170
|
+
"rel_type": "m.annotation",
|
|
171
|
+
"event_id": self.event_id,
|
|
172
|
+
"key": self.emoji,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
|
|
3
|
+
from nio import Event
|
|
4
|
+
from typing import TYPE_CHECKING, Optional, Any, List
|
|
5
|
+
|
|
6
|
+
from .errors import MatrixError
|
|
7
|
+
from .message import Message
|
|
8
|
+
from .room import Room
|
|
9
|
+
from .types import File, Image
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .bot import Bot # pragma: no cover
|
|
13
|
+
from .command import Command # pragma: no cover
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Context:
|
|
17
|
+
"""Represents the context in which a command is executed. Provides
|
|
18
|
+
access to the bot instance, room and event metadata, parsed arguments,
|
|
19
|
+
and other utilities.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, bot: "Bot", room: Room, event: Event):
|
|
23
|
+
self.bot = bot
|
|
24
|
+
self.room = room
|
|
25
|
+
self.event = event
|
|
26
|
+
|
|
27
|
+
self.body: str = getattr(event, "body", "")
|
|
28
|
+
self.sender: str = event.sender
|
|
29
|
+
|
|
30
|
+
# Command metadata
|
|
31
|
+
self.prefix: str = bot.prefix
|
|
32
|
+
self.command: Optional[Command] = None
|
|
33
|
+
self.subcommand: Optional[Command] = None
|
|
34
|
+
self._args: List[str] = shlex.split(self.body)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def args(self) -> List[str]:
|
|
38
|
+
"""
|
|
39
|
+
Returns the list of parsed arguments from the message body.
|
|
40
|
+
|
|
41
|
+
If a command is present, the command name is excluded.
|
|
42
|
+
|
|
43
|
+
:return: The list of arguments.
|
|
44
|
+
:rtype: List[str]
|
|
45
|
+
"""
|
|
46
|
+
if self.subcommand:
|
|
47
|
+
return self._args[2:]
|
|
48
|
+
|
|
49
|
+
if self.command:
|
|
50
|
+
return self._args[1:]
|
|
51
|
+
|
|
52
|
+
return self._args
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def logger(self) -> Any:
|
|
56
|
+
"""Logger for instance specific to the current room or event."""
|
|
57
|
+
return self.bot.log.getChild(self.room.room_id)
|
|
58
|
+
|
|
59
|
+
async def reply(
|
|
60
|
+
self,
|
|
61
|
+
content: str | None = None,
|
|
62
|
+
*,
|
|
63
|
+
raw: bool = False,
|
|
64
|
+
notice: bool = False,
|
|
65
|
+
file: File | None = None,
|
|
66
|
+
) -> Message:
|
|
67
|
+
"""Reply to the command with a message.
|
|
68
|
+
|
|
69
|
+
This is a convenience method that sends a message to the room where the
|
|
70
|
+
command was invoked. Supports text messages (with optional markdown
|
|
71
|
+
formatting) and file uploads (including images, videos, and audio).
|
|
72
|
+
|
|
73
|
+
See `Room.send()` for detailed usage examples and documentation.
|
|
74
|
+
|
|
75
|
+
## Example
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
@bot.command()
|
|
79
|
+
async def hello(ctx: Context):
|
|
80
|
+
await ctx.reply("Hello **world**!")
|
|
81
|
+
|
|
82
|
+
@bot.command()
|
|
83
|
+
async def status(ctx: Context):
|
|
84
|
+
await ctx.reply("Bot is online!", notice=True)
|
|
85
|
+
|
|
86
|
+
@bot.command()
|
|
87
|
+
async def cat(ctx: Context):
|
|
88
|
+
# Upload and send an image
|
|
89
|
+
from PIL import Image as PILImage
|
|
90
|
+
|
|
91
|
+
with PILImage.open("cat.jpg") as img:
|
|
92
|
+
width, height = img.size
|
|
93
|
+
|
|
94
|
+
with open("cat.jpg", "rb") as f:
|
|
95
|
+
resp, _ = await ctx.room.client.upload(f, content_type="image/jpeg")
|
|
96
|
+
|
|
97
|
+
image = Image(
|
|
98
|
+
path=resp.content_uri,
|
|
99
|
+
filename="cat.jpg",
|
|
100
|
+
mimetype="image/jpeg",
|
|
101
|
+
width=width,
|
|
102
|
+
height=height
|
|
103
|
+
)
|
|
104
|
+
await ctx.reply(file=image)
|
|
105
|
+
```
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
return await self.room.send(
|
|
110
|
+
content,
|
|
111
|
+
raw=raw,
|
|
112
|
+
notice=notice,
|
|
113
|
+
file=file,
|
|
114
|
+
)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
raise MatrixError(f"Failed to send message: {e}")
|
|
117
|
+
|
|
118
|
+
async def send_help(self) -> None:
|
|
119
|
+
"""Send help from the current command context.
|
|
120
|
+
|
|
121
|
+
Displays help text for the current subcommand, command, or the bot's
|
|
122
|
+
general help menu depending on what's available in the context. The help
|
|
123
|
+
hierarchy is: subcommand help > command help > bot help.
|
|
124
|
+
|
|
125
|
+
## Example
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
@bot.group()
|
|
129
|
+
async def config(ctx: Context):
|
|
130
|
+
# If user runs just "!config" with no subcommand
|
|
131
|
+
await ctx.send_help()
|
|
132
|
+
```
|
|
133
|
+
"""
|
|
134
|
+
if self.subcommand:
|
|
135
|
+
await self.reply(self.subcommand.help)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if self.command:
|
|
139
|
+
await self.reply(self.command.help)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
await self.bot.help.execute(self)
|
|
@@ -77,7 +77,7 @@ class Group(Command):
|
|
|
77
77
|
return cmd
|
|
78
78
|
|
|
79
79
|
async def invoke(self, ctx: "Context") -> None:
|
|
80
|
-
if subcommand := ctx.args.pop(0):
|
|
80
|
+
if ctx.args and (subcommand := ctx.args.pop(0)):
|
|
81
81
|
ctx.subcommand = self.get_command(subcommand)
|
|
82
82
|
await ctx.subcommand(ctx)
|
|
83
83
|
else:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional, List
|
|
1
|
+
from typing import Union, Optional, List
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
|
|
4
4
|
from matrix.context import Context
|
|
@@ -187,13 +187,9 @@ class HelpCommand(Command, ABC):
|
|
|
187
187
|
await ctx.reply(help_message)
|
|
188
188
|
|
|
189
189
|
def parse_help_arguments(
|
|
190
|
-
self, args: List[str]
|
|
190
|
+
self, args: List[str | int]
|
|
191
191
|
) -> tuple[Optional[str], Optional[str], int]:
|
|
192
|
-
"""Parse help command arguments to determine what to show.
|
|
193
|
-
|
|
194
|
-
:param args: List of arguments passed to help command
|
|
195
|
-
:return: Tuple of (command_name, subcommand_name, page_number)
|
|
196
|
-
"""
|
|
192
|
+
"""Parse help command arguments to determine what to show."""
|
|
197
193
|
command_name = None
|
|
198
194
|
subcommand_name = None
|
|
199
195
|
page_number = 1
|
|
@@ -201,29 +197,39 @@ class HelpCommand(Command, ABC):
|
|
|
201
197
|
if not args:
|
|
202
198
|
return command_name, subcommand_name, page_number
|
|
203
199
|
|
|
204
|
-
|
|
205
|
-
if len(args) == 1 and
|
|
206
|
-
|
|
200
|
+
first_arg = args[0]
|
|
201
|
+
if len(args) == 1 and (
|
|
202
|
+
isinstance(first_arg, int)
|
|
203
|
+
or (isinstance(first_arg, str) and first_arg.isdigit())
|
|
204
|
+
):
|
|
205
|
+
page_number = int(first_arg)
|
|
207
206
|
return command_name, subcommand_name, page_number
|
|
208
207
|
|
|
209
|
-
command_name =
|
|
208
|
+
command_name = str(first_arg)
|
|
210
209
|
|
|
211
210
|
if len(args) >= 2:
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
second_arg = args[1]
|
|
212
|
+
if isinstance(second_arg, int) or (
|
|
213
|
+
isinstance(second_arg, str) and second_arg.isdigit()
|
|
214
|
+
):
|
|
215
|
+
page_number = int(second_arg)
|
|
214
216
|
else:
|
|
215
|
-
subcommand_name =
|
|
217
|
+
subcommand_name = str(second_arg)
|
|
216
218
|
|
|
217
|
-
if len(args) >= 3
|
|
218
|
-
|
|
219
|
+
if len(args) >= 3:
|
|
220
|
+
third_arg = args[2]
|
|
221
|
+
if isinstance(third_arg, int) or (
|
|
222
|
+
isinstance(third_arg, str) and third_arg.isdigit()
|
|
223
|
+
):
|
|
224
|
+
page_number = int(third_arg)
|
|
219
225
|
|
|
220
226
|
return command_name, subcommand_name, page_number
|
|
221
227
|
|
|
222
228
|
async def execute(
|
|
223
229
|
self,
|
|
224
230
|
ctx: Context,
|
|
225
|
-
cmd_or_page: str
|
|
226
|
-
subcommand: str | None = None,
|
|
231
|
+
cmd_or_page: Union[str, int, None] = None,
|
|
232
|
+
subcommand: Union[str | None] = None,
|
|
227
233
|
) -> None:
|
|
228
234
|
"""
|
|
229
235
|
Execute the help command using show_command_help and show_group_help.
|