matrix-python 1.2.0a0__tar.gz → 1.3.3a0__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.3.3a0/.github/dependabot.yml +6 -0
- matrix_python-1.3.3a0/.github/workflows/CODEOWNERS +1 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/PKG-INFO +8 -10
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/README.md +7 -9
- matrix_python-1.3.3a0/examples/extension.py +45 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/__init__.py +2 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/_version.py +3 -3
- matrix_python-1.3.3a0/matrix/bot.py +288 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/context.py +0 -1
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/errors.py +13 -6
- matrix_python-1.3.3a0/matrix/extension.py +56 -0
- matrix_python-1.3.3a0/matrix/registry.py +333 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/scheduler.py +6 -1
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix_python.egg-info/PKG-INFO +8 -10
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix_python.egg-info/SOURCES.txt +7 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/pyproject.toml +7 -1
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/test_bot.py +321 -3
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/test_context.py +0 -1
- matrix_python-1.3.3a0/tests/test_extension.py +159 -0
- matrix_python-1.3.3a0/tests/test_registry.py +362 -0
- matrix_python-1.2.0a0/matrix/bot.py +0 -496
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/.github/workflows/codeql.yml +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/.github/workflows/publish.yml +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/.github/workflows/scorecard.yml +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/.github/workflows/tests.yml +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/.gitignore +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/CODE_OF_CONDUCT.md +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/CONTRIBUTING.md +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/LICENSE +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/README.md +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/checks.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/config.yaml +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/cooldown.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/error_handling.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/ping.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/reaction.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/examples/scheduler.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/checks.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/command.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/config.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/content.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/group.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/help/__init__.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/help/help_command.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/help/pagination.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/message.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/room.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix/types.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix_python.egg-info/dependency_links.txt +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix_python.egg-info/requires.txt +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/matrix_python.egg-info/top_level.txt +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/mypy.ini +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/setup.cfg +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/config_fixture.yaml +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/config_fixture_token.yaml +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/help/test_default_help_command.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/help/test_help_command.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/help/test_pagination.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/test_command.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/test_config.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/test_group.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/test_message.py +0 -0
- {matrix_python-1.2.0a0 → matrix_python-1.3.3a0}/tests/test_room.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @PenguinBoi12
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrix-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.3a0
|
|
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>
|
|
@@ -697,8 +697,6 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
697
697
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
698
698
|
Requires-Dist: types-Markdown; extra == "dev"
|
|
699
699
|
|
|
700
|
-
<h1 align="center">Matrix.py</h1>
|
|
701
|
-
|
|
702
700
|
<div align="center">
|
|
703
701
|
<em>A simple, developer-friendly library to create powerful <a href="https://matrix.org">Matrix</a> bots.</em>
|
|
704
702
|
</div>
|
|
@@ -708,13 +706,14 @@ Requires-Dist: types-Markdown; extra == "dev"
|
|
|
708
706
|
<hr />
|
|
709
707
|
|
|
710
708
|
[](https://github.com/Code-Society-Lab/matrixpy/wiki)
|
|
711
|
-
[](https://discord.gg/code-society-823178343943897088)
|
|
710
|
+
[](https://matrix.to/#/%23codesociety:matrix.org )
|
|
712
711
|
[](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/tests.yml)
|
|
713
712
|
[](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/codeql.yml)
|
|
714
713
|
[](https://securityscorecards.dev/viewer/?uri=github.com/Code-Society-Lab/matrixpy)
|
|
715
714
|
|
|
716
715
|
Matrix.py is a lightweight and intuitive Python library to build bots on
|
|
717
|
-
the [Matrix protocol](
|
|
716
|
+
the [Matrix protocol](https://matrix.org). It provides a clean,
|
|
718
717
|
decorator-based API similar to popular event-driven frameworks, allowing
|
|
719
718
|
developers to focus on behavior rather than boilerplate.
|
|
720
719
|
|
|
@@ -736,7 +735,7 @@ pip install matrix-python
|
|
|
736
735
|
|
|
737
736
|
If you plan on contributing to matrix.py, we recommend to install the development libraries:
|
|
738
737
|
```
|
|
739
|
-
pip install .[
|
|
738
|
+
pip install -e .[dev]
|
|
740
739
|
```
|
|
741
740
|
|
|
742
741
|
*Note*: It is recommended to use a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/) when installing python packages.
|
|
@@ -745,19 +744,18 @@ pip install .[env]
|
|
|
745
744
|
```python
|
|
746
745
|
from matrix import Bot, Context
|
|
747
746
|
|
|
748
|
-
bot = Bot(
|
|
747
|
+
bot = Bot(config="config.yml")
|
|
749
748
|
|
|
750
749
|
|
|
751
750
|
@bot.command("ping")
|
|
752
751
|
async def ping(ctx: Context):
|
|
753
|
-
print(f"{ctx.sender} invoked {ctx.body} in room {ctx.room_name}.")
|
|
754
752
|
await ctx.send("Pong!")
|
|
755
753
|
|
|
756
754
|
|
|
757
755
|
bot.start()
|
|
758
756
|
```
|
|
759
757
|
|
|
760
|
-
[Documentation](https://github.com/Code-Society-Lab/matrixpy/wiki) - [Examples](
|
|
758
|
+
[Documentation](https://github.com/Code-Society-Lab/matrixpy/wiki) - [Examples](https://github.com/Code-Society-Lab/matrixpy/tree/main/examples)
|
|
761
759
|
|
|
762
760
|
# Contributing
|
|
763
761
|
We welcome everyone to contribute!
|
|
@@ -765,7 +763,7 @@ We welcome everyone to contribute!
|
|
|
765
763
|
Whether it's fixing bugs, suggesting features, or improving the docs - every bit helps.
|
|
766
764
|
- Submit an issue
|
|
767
765
|
- Open a pull request
|
|
768
|
-
- Or just hop into our [Discord
|
|
766
|
+
- Or just hop into our [Matrix](https://matrix.to/#/%23codesociety:matrix.org) or [Discord](https://discord.gg/code-society-823178343943897088) server and say hi!
|
|
769
767
|
|
|
770
768
|
If you intend to contribute, please read the [CONTRIBUTING.md](./CONTRIBUTING.md) first. Additionally, **every contributor** is expected to follow the [code of conduct](./CODE_OF_CONDUCT.md).
|
|
771
769
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<h1 align="center">Matrix.py</h1>
|
|
2
|
-
|
|
3
1
|
<div align="center">
|
|
4
2
|
<em>A simple, developer-friendly library to create powerful <a href="https://matrix.org">Matrix</a> bots.</em>
|
|
5
3
|
</div>
|
|
@@ -9,13 +7,14 @@
|
|
|
9
7
|
<hr />
|
|
10
8
|
|
|
11
9
|
[](https://github.com/Code-Society-Lab/matrixpy/wiki)
|
|
12
|
-
[](https://discord.gg/code-society-823178343943897088)
|
|
11
|
+
[](https://matrix.to/#/%23codesociety:matrix.org )
|
|
13
12
|
[](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/tests.yml)
|
|
14
13
|
[](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/codeql.yml)
|
|
15
14
|
[](https://securityscorecards.dev/viewer/?uri=github.com/Code-Society-Lab/matrixpy)
|
|
16
15
|
|
|
17
16
|
Matrix.py is a lightweight and intuitive Python library to build bots on
|
|
18
|
-
the [Matrix protocol](
|
|
17
|
+
the [Matrix protocol](https://matrix.org). It provides a clean,
|
|
19
18
|
decorator-based API similar to popular event-driven frameworks, allowing
|
|
20
19
|
developers to focus on behavior rather than boilerplate.
|
|
21
20
|
|
|
@@ -37,7 +36,7 @@ pip install matrix-python
|
|
|
37
36
|
|
|
38
37
|
If you plan on contributing to matrix.py, we recommend to install the development libraries:
|
|
39
38
|
```
|
|
40
|
-
pip install .[
|
|
39
|
+
pip install -e .[dev]
|
|
41
40
|
```
|
|
42
41
|
|
|
43
42
|
*Note*: It is recommended to use a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/) when installing python packages.
|
|
@@ -46,19 +45,18 @@ pip install .[env]
|
|
|
46
45
|
```python
|
|
47
46
|
from matrix import Bot, Context
|
|
48
47
|
|
|
49
|
-
bot = Bot(
|
|
48
|
+
bot = Bot(config="config.yml")
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
@bot.command("ping")
|
|
53
52
|
async def ping(ctx: Context):
|
|
54
|
-
print(f"{ctx.sender} invoked {ctx.body} in room {ctx.room_name}.")
|
|
55
53
|
await ctx.send("Pong!")
|
|
56
54
|
|
|
57
55
|
|
|
58
56
|
bot.start()
|
|
59
57
|
```
|
|
60
58
|
|
|
61
|
-
[Documentation](https://github.com/Code-Society-Lab/matrixpy/wiki) - [Examples](
|
|
59
|
+
[Documentation](https://github.com/Code-Society-Lab/matrixpy/wiki) - [Examples](https://github.com/Code-Society-Lab/matrixpy/tree/main/examples)
|
|
62
60
|
|
|
63
61
|
# Contributing
|
|
64
62
|
We welcome everyone to contribute!
|
|
@@ -66,7 +64,7 @@ We welcome everyone to contribute!
|
|
|
66
64
|
Whether it's fixing bugs, suggesting features, or improving the docs - every bit helps.
|
|
67
65
|
- Submit an issue
|
|
68
66
|
- Open a pull request
|
|
69
|
-
- Or just hop into our [Discord
|
|
67
|
+
- Or just hop into our [Matrix](https://matrix.to/#/%23codesociety:matrix.org) or [Discord](https://discord.gg/code-society-823178343943897088) server and say hi!
|
|
70
68
|
|
|
71
69
|
If you intend to contribute, please read the [CONTRIBUTING.md](./CONTRIBUTING.md) first. Additionally, **every contributor** is expected to follow the [code of conduct](./CODE_OF_CONDUCT.md).
|
|
72
70
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from matrix import Extension, Context
|
|
2
|
+
|
|
3
|
+
extension = Extension("math")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@extension.group("math", description="Math Group")
|
|
7
|
+
async def math_group(ctx: Context):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@math_group.command()
|
|
12
|
+
async def add(ctx: Context, a: int, b: int):
|
|
13
|
+
await ctx.reply(f"**{a} + {b} = {a + b}**")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@math_group.command()
|
|
17
|
+
async def subtract(ctx: Context, a: int, b: int):
|
|
18
|
+
await ctx.reply(f"{a} - {b} = {a - b}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@math_group.command()
|
|
22
|
+
async def multiply(ctx: Context, a: int, b: int):
|
|
23
|
+
await ctx.reply(f"{a} x {b} = {a * b}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@math_group.command()
|
|
27
|
+
async def divide(ctx: Context, a: int, b: int):
|
|
28
|
+
await ctx.reply(f"{a} ÷ {b} = {a / b}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@divide.error(ZeroDivisionError)
|
|
32
|
+
async def divide_error(ctx: Context, error):
|
|
33
|
+
await ctx.reply(f"Divide error: {error}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
from matrix import Bot
|
|
38
|
+
from math_extension import extension as math_extension
|
|
39
|
+
|
|
40
|
+
bot = Bot(config="config.yaml")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
bot.load_extension(math_extension)
|
|
44
|
+
bot.start()
|
|
45
|
+
"""
|
|
@@ -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 .extension import Extension
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
|
20
21
|
"Bot",
|
|
@@ -26,4 +27,5 @@ __all__ = [
|
|
|
26
27
|
"HelpCommand",
|
|
27
28
|
"cooldown",
|
|
28
29
|
"Room",
|
|
30
|
+
"Extension",
|
|
29
31
|
]
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.3.3a0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 3, 3, 'a0')
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g7d083d049'
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import inspect
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from typing import Union, Optional
|
|
7
|
+
|
|
8
|
+
from nio import AsyncClient, Event, MatrixRoom
|
|
9
|
+
|
|
10
|
+
from .room import Room
|
|
11
|
+
from .group import Group
|
|
12
|
+
from .config import Config
|
|
13
|
+
from .context import Context
|
|
14
|
+
from .extension import Extension
|
|
15
|
+
from .registry import Registry
|
|
16
|
+
from .help import HelpCommand, DefaultHelpCommand
|
|
17
|
+
from .scheduler import Scheduler
|
|
18
|
+
from .errors import AlreadyRegisteredError, CommandNotFoundError, CheckError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Bot(Registry):
|
|
22
|
+
"""
|
|
23
|
+
The base class defining a Matrix bot.
|
|
24
|
+
|
|
25
|
+
This class manages the connection to a Matrix homeserver, listens
|
|
26
|
+
for events, and dispatches them to registered handlers. It also supports
|
|
27
|
+
a command system with decorators for easy registration.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self, *, config: Union[Config, str], help: Optional[HelpCommand] = None
|
|
32
|
+
) -> None:
|
|
33
|
+
if isinstance(config, Config):
|
|
34
|
+
self.config = config
|
|
35
|
+
elif isinstance(config, str):
|
|
36
|
+
self.config = Config(config_path=config)
|
|
37
|
+
else:
|
|
38
|
+
raise TypeError("config must be a Config instance or a config file path")
|
|
39
|
+
|
|
40
|
+
super().__init__(self.__class__.__name__, prefix=self.config.prefix)
|
|
41
|
+
|
|
42
|
+
self.client: AsyncClient = AsyncClient(self.config.homeserver)
|
|
43
|
+
self.extensions: dict[str, Extension] = {}
|
|
44
|
+
self.scheduler: Scheduler = Scheduler()
|
|
45
|
+
self.log: logging.Logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
self.start_at: float | None = None # unix timestamp
|
|
48
|
+
|
|
49
|
+
self.help: HelpCommand = help or DefaultHelpCommand(prefix=self.prefix)
|
|
50
|
+
self.register_command(self.help)
|
|
51
|
+
|
|
52
|
+
self.client.add_event_callback(self._on_event, Event)
|
|
53
|
+
self._auto_register_events()
|
|
54
|
+
|
|
55
|
+
def get_room(self, room_id: str) -> Room:
|
|
56
|
+
"""Retrieve a Room instance based on the room_id."""
|
|
57
|
+
matrix_room = self.client.rooms[room_id]
|
|
58
|
+
return Room(matrix_room=matrix_room, client=self.client)
|
|
59
|
+
|
|
60
|
+
def load_extension(self, extension: Extension) -> None:
|
|
61
|
+
self.log.debug(f"Loading extension: '{extension.name}'")
|
|
62
|
+
|
|
63
|
+
if extension.name in self.extensions:
|
|
64
|
+
raise AlreadyRegisteredError(extension)
|
|
65
|
+
|
|
66
|
+
for cmd in extension._commands.values():
|
|
67
|
+
if isinstance(cmd, Group):
|
|
68
|
+
self.register_group(cmd)
|
|
69
|
+
else:
|
|
70
|
+
self.register_command(cmd)
|
|
71
|
+
|
|
72
|
+
for event_type, handlers in extension._event_handlers.items():
|
|
73
|
+
self._event_handlers[event_type].extend(handlers)
|
|
74
|
+
|
|
75
|
+
self._checks.extend(extension._checks)
|
|
76
|
+
self._error_handlers.update(extension._error_handlers)
|
|
77
|
+
self._command_error_handlers.update(extension._command_error_handlers)
|
|
78
|
+
|
|
79
|
+
for job in extension._scheduler.jobs:
|
|
80
|
+
self.scheduler.scheduler.add_job(
|
|
81
|
+
job.func,
|
|
82
|
+
trigger=job.trigger,
|
|
83
|
+
name=job.name,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self.extensions[extension.name] = extension
|
|
87
|
+
extension.load()
|
|
88
|
+
self.log.debug("loaded extension '%s'", extension.name)
|
|
89
|
+
|
|
90
|
+
def unload_extension(self, ext_name: str) -> None:
|
|
91
|
+
self.log.debug("Unloading extension: '%s'", ext_name)
|
|
92
|
+
|
|
93
|
+
extension = self.extensions.pop(ext_name, None)
|
|
94
|
+
if extension is None:
|
|
95
|
+
raise ValueError(f"No extension named '{ext_name}' is loaded")
|
|
96
|
+
|
|
97
|
+
for cmd_name in extension._commands:
|
|
98
|
+
self._commands.pop(cmd_name, None)
|
|
99
|
+
|
|
100
|
+
for event_type, handlers in extension._event_handlers.items():
|
|
101
|
+
for handler in handlers:
|
|
102
|
+
self._event_handlers[event_type].remove(handler)
|
|
103
|
+
|
|
104
|
+
for check in extension._checks:
|
|
105
|
+
self._checks.remove(check)
|
|
106
|
+
|
|
107
|
+
for exc_type in extension._error_handlers:
|
|
108
|
+
self._error_handlers.pop(exc_type, None)
|
|
109
|
+
|
|
110
|
+
for exc_type in extension._command_error_handlers:
|
|
111
|
+
self._command_error_handlers.pop(exc_type, None)
|
|
112
|
+
|
|
113
|
+
for job in extension._scheduler.jobs:
|
|
114
|
+
bot_job = next((j for j in self.scheduler.jobs if j.func is job.func), None)
|
|
115
|
+
if bot_job:
|
|
116
|
+
bot_job.remove()
|
|
117
|
+
|
|
118
|
+
extension.unload()
|
|
119
|
+
self.log.debug("unloaded extension '%s'", ext_name)
|
|
120
|
+
|
|
121
|
+
def _auto_register_events(self) -> None:
|
|
122
|
+
for attr in dir(self):
|
|
123
|
+
if not attr.startswith("on_"):
|
|
124
|
+
continue
|
|
125
|
+
coro = getattr(self, attr, None)
|
|
126
|
+
if inspect.iscoroutinefunction(coro):
|
|
127
|
+
try:
|
|
128
|
+
self.event(coro)
|
|
129
|
+
except ValueError: # ignore unknown name
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
async def _on_event(self, room: MatrixRoom, event: Event) -> None:
|
|
133
|
+
# ignore bot events
|
|
134
|
+
if event.sender == self.client.user:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# ignore events that happened before the bot started
|
|
138
|
+
if self.start_at and self.start_at > (event.server_timestamp / 1000):
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
await self._dispatch(room, event)
|
|
143
|
+
except Exception as error:
|
|
144
|
+
await self.on_error(error)
|
|
145
|
+
|
|
146
|
+
async def _dispatch(self, room: MatrixRoom, event: Event) -> None:
|
|
147
|
+
"""Internal type-based fan-out plus optional command handling."""
|
|
148
|
+
for event_type, funcs in self._event_handlers.items():
|
|
149
|
+
if isinstance(event, event_type):
|
|
150
|
+
for func in funcs:
|
|
151
|
+
await func(room, event)
|
|
152
|
+
|
|
153
|
+
async def _process_commands(self, room: MatrixRoom, event: Event) -> None:
|
|
154
|
+
"""Parse and execute commands"""
|
|
155
|
+
ctx = await self._build_context(room, event)
|
|
156
|
+
|
|
157
|
+
if ctx.command:
|
|
158
|
+
for check in self._checks:
|
|
159
|
+
if not await check(ctx):
|
|
160
|
+
raise CheckError(ctx.command, check)
|
|
161
|
+
|
|
162
|
+
await ctx.command(ctx)
|
|
163
|
+
|
|
164
|
+
async def _build_context(self, matrix_room: MatrixRoom, event: Event) -> Context:
|
|
165
|
+
room = self.get_room(matrix_room.room_id)
|
|
166
|
+
ctx = Context(bot=self, room=room, event=event)
|
|
167
|
+
prefix: str | None = None
|
|
168
|
+
|
|
169
|
+
if self.prefix is not None and ctx.body.startswith(self.prefix):
|
|
170
|
+
prefix = self.prefix
|
|
171
|
+
else:
|
|
172
|
+
prefix = next(
|
|
173
|
+
(
|
|
174
|
+
cmd.prefix
|
|
175
|
+
for cmd in self._commands.values()
|
|
176
|
+
if cmd.prefix is not None and ctx.body.startswith(cmd.prefix)
|
|
177
|
+
),
|
|
178
|
+
self.config.prefix,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if prefix is None or not ctx.body.startswith(prefix):
|
|
182
|
+
return ctx
|
|
183
|
+
|
|
184
|
+
if parts := ctx.body[len(prefix) :].split():
|
|
185
|
+
cmd_name = parts[0]
|
|
186
|
+
cmd = self._commands.get(cmd_name)
|
|
187
|
+
|
|
188
|
+
if cmd and cmd.prefix and not ctx.body.startswith(cmd.prefix):
|
|
189
|
+
return ctx
|
|
190
|
+
|
|
191
|
+
if not cmd:
|
|
192
|
+
raise CommandNotFoundError(cmd_name)
|
|
193
|
+
|
|
194
|
+
ctx.command = cmd
|
|
195
|
+
|
|
196
|
+
return ctx
|
|
197
|
+
|
|
198
|
+
async def on_message(self, room: MatrixRoom, event: Event) -> None:
|
|
199
|
+
"""
|
|
200
|
+
Invoked when a message event is received.
|
|
201
|
+
|
|
202
|
+
This method is automatically called when a :class:`nio.RoomMessageText`
|
|
203
|
+
event is detected. It is primarily responsible for detecting and
|
|
204
|
+
processing commands that match the bot's defined prefix.
|
|
205
|
+
|
|
206
|
+
:param ctx: The context object containing information about the Matrix
|
|
207
|
+
room and the message event.
|
|
208
|
+
:type ctx: Context
|
|
209
|
+
"""
|
|
210
|
+
await self._process_commands(room, event)
|
|
211
|
+
|
|
212
|
+
async def on_ready(self) -> None:
|
|
213
|
+
"""Invoked after a successful login, before sync starts."""
|
|
214
|
+
self.log.info("bot is ready")
|
|
215
|
+
|
|
216
|
+
async def on_error(self, error: Exception) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Handle errors by invoking a registered error handler,
|
|
219
|
+
a generic error callback, or logging the exception.
|
|
220
|
+
|
|
221
|
+
:param error: The exception instance that was raised.
|
|
222
|
+
:type error: Exceptipon
|
|
223
|
+
"""
|
|
224
|
+
if handler := self._error_handlers.get(type(error)):
|
|
225
|
+
await handler(error)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if self._on_error:
|
|
229
|
+
await self._on_error(error)
|
|
230
|
+
return
|
|
231
|
+
self.log.exception("Unhandled error: '%s'", error)
|
|
232
|
+
|
|
233
|
+
async def on_command_error(self, ctx: "Context", error: Exception) -> None:
|
|
234
|
+
"""
|
|
235
|
+
Handles errors raised during command invocation.
|
|
236
|
+
|
|
237
|
+
This method is called automatically when a command error occurs.
|
|
238
|
+
If a specific error handler is registered for the type of the
|
|
239
|
+
exception, it will be invoked with the current context and error.
|
|
240
|
+
|
|
241
|
+
:param ctx: The context in which the command was invoked.
|
|
242
|
+
:type ctx: Context
|
|
243
|
+
:param error: The exception that was raised during command execution.
|
|
244
|
+
:type error: Exception
|
|
245
|
+
"""
|
|
246
|
+
if handler := self._command_error_handlers.get(type(error)):
|
|
247
|
+
await handler(ctx, error)
|
|
248
|
+
|
|
249
|
+
async def run(self) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Log in to the Matrix homeserver and begin syncing events.
|
|
252
|
+
|
|
253
|
+
This method should be used within an asynchronous context,
|
|
254
|
+
typically via :func:`asyncio.run`. It handles authentication,
|
|
255
|
+
calls the :meth:`on_ready` hook, and starts the long-running
|
|
256
|
+
sync loop for receiving events.
|
|
257
|
+
"""
|
|
258
|
+
self.client.user = self.config.user_id
|
|
259
|
+
|
|
260
|
+
self.start_at = time.time()
|
|
261
|
+
self.log.info("starting – timestamp=%s", self.start_at)
|
|
262
|
+
|
|
263
|
+
if self.config.token:
|
|
264
|
+
self.client.access_token = self.config.token
|
|
265
|
+
else:
|
|
266
|
+
login_resp = await self.client.login(self.config.password)
|
|
267
|
+
self.log.info("logged in: %s", login_resp)
|
|
268
|
+
|
|
269
|
+
self.scheduler.start()
|
|
270
|
+
|
|
271
|
+
await self.on_ready()
|
|
272
|
+
await self.client.sync_forever(timeout=30_000)
|
|
273
|
+
|
|
274
|
+
def start(self) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Synchronous entry point for running the bot.
|
|
277
|
+
|
|
278
|
+
This is a convenience wrapper that allows running the bot like a
|
|
279
|
+
script using a blocking call. It internally calls :meth:`run` within
|
|
280
|
+
:func:`asyncio.run`, and ensures the client is closed gracefully
|
|
281
|
+
on interruption.
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
asyncio.run(self.run())
|
|
285
|
+
except KeyboardInterrupt:
|
|
286
|
+
self.log.info("bot interrupted by user")
|
|
287
|
+
finally:
|
|
288
|
+
asyncio.run(self.client.close())
|
|
@@ -4,6 +4,7 @@ import inspect
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
5
|
from .command import Command # pragma: no cover
|
|
6
6
|
from .group import Group # pragma: no cover
|
|
7
|
+
from .extension import Extension
|
|
7
8
|
|
|
8
9
|
Callback = Callable[..., Coroutine[Any, Any, Any]]
|
|
9
10
|
|
|
@@ -12,6 +13,17 @@ class MatrixError(Exception):
|
|
|
12
13
|
pass
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
class RegistryError(MatrixError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AlreadyRegisteredError(RegistryError):
|
|
21
|
+
def __init__(self, entry: "Command | Group | Extension"):
|
|
22
|
+
super().__init__(
|
|
23
|
+
f"{entry.__class__.__name__} '{entry.name}' is already registered"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
15
27
|
class CommandError(MatrixError):
|
|
16
28
|
pass
|
|
17
29
|
|
|
@@ -21,7 +33,7 @@ class CommandNotFoundError(CommandError):
|
|
|
21
33
|
super().__init__(f"Command with name '{cmd}' not found")
|
|
22
34
|
|
|
23
35
|
|
|
24
|
-
class
|
|
36
|
+
class CommandAlreadyRegisteredError(CommandError):
|
|
25
37
|
def __init__(self, cmd: "Command"):
|
|
26
38
|
super().__init__(f"Command '{cmd}' is already registered")
|
|
27
39
|
|
|
@@ -40,11 +52,6 @@ class GroupError(CommandError):
|
|
|
40
52
|
pass
|
|
41
53
|
|
|
42
54
|
|
|
43
|
-
class GroupAlreadyRegisteredError(GroupError):
|
|
44
|
-
def __init__(self, group: "Group"):
|
|
45
|
-
super().__init__(f"Group '{group}' is already registered")
|
|
46
|
-
|
|
47
|
-
|
|
48
55
|
class ConfigError(MatrixError):
|
|
49
56
|
def __init__(self, error: str):
|
|
50
57
|
super().__init__(f"Missing required configuration: '{error}'")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Coroutine, Optional
|
|
5
|
+
from matrix.registry import Registry
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Extension(Registry):
|
|
11
|
+
def __init__(self, name: str, prefix: Optional[str] = None) -> None:
|
|
12
|
+
super().__init__(name, prefix=prefix)
|
|
13
|
+
self._on_load: Optional[Callable] = None
|
|
14
|
+
self._on_unload: Optional[Callable] = None
|
|
15
|
+
|
|
16
|
+
def load(self) -> None:
|
|
17
|
+
if self._on_load:
|
|
18
|
+
self._on_load()
|
|
19
|
+
|
|
20
|
+
def on_load(self, func: Callable) -> Callable:
|
|
21
|
+
"""Decorator to register a function to be called after this extension
|
|
22
|
+
is loaded into the bot.
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
@extension.on_load
|
|
28
|
+
def setup():
|
|
29
|
+
print("extension loaded")
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
if inspect.iscoroutinefunction(func):
|
|
33
|
+
raise TypeError("on_load handler must not be a coroutine")
|
|
34
|
+
self._on_load = func
|
|
35
|
+
return func
|
|
36
|
+
|
|
37
|
+
def unload(self) -> None:
|
|
38
|
+
if self._on_unload:
|
|
39
|
+
self._on_unload()
|
|
40
|
+
|
|
41
|
+
def on_unload(self, func: Callable) -> Callable:
|
|
42
|
+
"""Decorator to register a function to be called before this extension
|
|
43
|
+
is unloaded from the bot.
|
|
44
|
+
|
|
45
|
+
## Example
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
@extension.on_unload
|
|
49
|
+
def teardown():
|
|
50
|
+
print("extension unloaded")
|
|
51
|
+
```
|
|
52
|
+
"""
|
|
53
|
+
if inspect.iscoroutinefunction(func):
|
|
54
|
+
raise TypeError("on_unload handler must not be a coroutine")
|
|
55
|
+
self._on_unload = func
|
|
56
|
+
return func
|