Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ decorator-based API similar to popular event-driven frameworks, allowing
developers to focus on behavior rather than boilerplate.

#### Key Features

- Minimal setup, easy to extend
- Event-driven API using async/await
- Clean command registration
Expand All @@ -28,45 +29,53 @@ developers to focus on behavior rather than boilerplate.
# Quickstart

**Requirements**

- Python 3.10+

```
pip install matrix-python
```

If you plan on contributing to matrix.py, we recommend to install the development libraries:

```
pip install -e .[dev]
```

*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.

*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.

```python
from matrix import Bot, Context

bot = Bot(config="config.yml")
bot = Bot()


@bot.command("ping")
async def ping(ctx: Context):
await ctx.send("Pong!")
await ctx.reply("Pong!")


bot.start()
bot.start(config="config.yml")
```

[Documentation](https://github.com/Code-Society-Lab/matrixpy/wiki) - [Examples](https://github.com/Code-Society-Lab/matrixpy/tree/main/examples)

# Contributing
We welcome everyone to contribute!

We welcome everyone to contribute!

Whether it's fixing bugs, suggesting features, or improving the docs - every bit helps.

- Submit an issue
- Open a pull request
- Or just hop into our [Matrix](https://matrix.to/#/%23codesociety:matrix.org) or [Discord](https://discord.gg/code-society-823178343943897088) server and say hi!
- Or just hop into our [Matrix](https://matrix.to/#/%23codesociety:matrix.org)
or [Discord](https://discord.gg/code-society-823178343943897088) server and say hi!

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).
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).

# License

matrix.py is released under [GPL-3.0](https://opensource.org/license/gpl-3-0)
4 changes: 2 additions & 2 deletions examples/checks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from matrix import Bot, Context
from matrix.errors import CheckError

bot = Bot(config="config.yaml")
bot = Bot()

allowed_users = {"@alice:matrix.org", "@bob:matrix.org"}

Expand All @@ -23,4 +23,4 @@ async def permission_error_handler(ctx: Context, error: CheckError) -> None:
await ctx.reply(f"Access denied: {error}")


bot.start()
bot.start(config="config.yaml")
4 changes: 2 additions & 2 deletions examples/cooldown.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from matrix import Bot, Context, cooldown
from matrix.errors import CooldownError

bot = Bot(config="config.yaml")
bot = Bot()


# Invoke by using !hello
Expand All @@ -27,4 +27,4 @@ async def cooldown_function(ctx: Context, error: CooldownError) -> None:
await ctx.reply(f"⏳ Try again in {error.retry:.1f}s")


bot.start()
bot.start(config="config.yaml")
4 changes: 2 additions & 2 deletions examples/error_handling.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from matrix import Bot, Context
from matrix.errors import CommandNotFoundError, MissingArgumentError

bot = Bot(config="config.yaml")
bot = Bot()


@bot.error(CommandNotFoundError)
Expand Down Expand Up @@ -30,4 +30,4 @@ async def command_error(ctx: Context, error: MissingArgumentError) -> None:
await ctx.reply(f"{error}")


bot.start()
bot.start(config="config.yaml")
6 changes: 4 additions & 2 deletions examples/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ async def divide_error(ctx: Context, error):


"""
# bot.py

from matrix import Bot
from math_extension import extension as math_extension

bot = Bot(config="config.yaml")
bot = Bot()


bot.load_extension(math_extension)
bot.start()
bot.start(config="config.yaml")
"""
4 changes: 2 additions & 2 deletions examples/ping.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from matrix import Bot, Context

bot = Bot(config="config.yaml")
bot = Bot()


@bot.command("ping")
async def ping(ctx: Context) -> None:
await ctx.reply("Pong!")


bot.start()
bot.start(config="config.yaml")
39 changes: 24 additions & 15 deletions examples/reaction.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,52 @@
from asyncio import Event
from matrix import Bot, Room
from nio import MatrixRoom, RoomMessageText, ReactionEvent
from matrix import Bot
from matrix.message import Message

bot = Bot(config="config.yaml")
bot = Bot()


@bot.event
async def on_message(room: Room, event: Event) -> None:
async def on_message(room: MatrixRoom, event: RoomMessageText) -> None:
"""
This function listens for new messages in a room and reacts based
on the message content.
"""
room = bot.get_room(room.room_id)
message = Message(
room=room,
event_id=event.event_id,
body=event.body,
client=bot.client,
)

if event.body.lower().startswith("thanks"):
await room.send(event=event, key="🙏")
await message.react("🙏")

if event.body.lower().startswith("hello"):
# Can also react with a text message instead of emoji
await room.send(event=event, key="hi")
await message.react("hi")

if event.body.lower().startswith("❤️"):
await room.send(event=event, message="❤️")
await message.react("❤️")


@bot.event
async def on_react(room: Room, event: Event) -> None:
async def on_react(room: MatrixRoom, event: ReactionEvent) -> None:
"""
This function listens for new member reaction to messages in a room,
and reacts based on the reaction emoji.
"""
room = bot.get_room(room.room_id)

message = Message(
room=room,
event_id=event.event_id,
body=None,
client=bot.client,
)
emoji = event.key
event_id = event.source["content"]["m.relates_to"]["event_id"]

if emoji == "🙏":
await room.send(event=event_id, key="❤️")

if emoji == "❤️":
await room.send(message="❤️")
await message.react("❤️")


bot.start()
bot.start(config="config.yaml")
10 changes: 5 additions & 5 deletions examples/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from matrix import Bot, Context

bot = Bot(config="config.yaml")
bot = Bot()

room_id = "!your_room_id:matrix.org" # Replace with your room ID

Expand All @@ -15,19 +15,19 @@ async def ping(ctx: Context) -> None:
@bot.schedule("* * * * *")
async def scheduled_task() -> None:
# This task runs every minute.
await room.send(message="Scheduled ping!")
await room.send("Scheduled ping!")


@bot.schedule("0 * * * *")
async def hourly_task() -> None:
# This task runs every hour.
await room.send(message="This is your hourly update!")
await room.send("This is your hourly update!")


@bot.schedule("0 9 * * 1-5")
async def weekday_morning_task() -> None:
# This task runs every weekday at 9 AM.
await room.send(message="Good morning! Here's your weekday update!")
await room.send("Good morning! Here's your weekday update!")


bot.start()
bot.start(config="config.yaml")
59 changes: 43 additions & 16 deletions matrix/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,34 @@ class Bot(Registry):
"""

def __init__(
self, *, config: Union[Config, str], help: Optional[HelpCommand] = None
self,
*,
help_: Optional[HelpCommand] = None,
) -> None:
if isinstance(config, Config):
self.config = config
elif isinstance(config, str):
self.config = Config(config_path=config)
else:
raise TypeError("config must be a Config instance or a config file path")
super().__init__(self.__class__.__name__)

super().__init__(self.__class__.__name__, prefix=self.config.prefix)

self.client: AsyncClient = AsyncClient(self.config.homeserver)
self._config: Config | None = None
self._client: AsyncClient | None = None
self._help: HelpCommand | None = help_
self.extensions: dict[str, Extension] = {}
self.scheduler: Scheduler = Scheduler()
self.log: logging.Logger = logging.getLogger(__name__)
self.start_at: float | None = None

self.start_at: float | None = None # unix timestamp
@property
def client(self) -> AsyncClient:
assert self._client is not None, "Bot has not been started."
return self._client

self.help: HelpCommand = help or DefaultHelpCommand(prefix=self.prefix)
self.register_command(self.help)
@property
def config(self) -> Config:
assert self._config is not None, "Bot has not been started."
return self._config

self.client.add_event_callback(self._on_matrix_event, Event)
self._auto_register_events()
@property
def help(self) -> HelpCommand:
assert self._help is not None, "Bot has not been started."
return self._help

def _auto_register_events(self) -> None:
for attr in dir(self):
Expand Down Expand Up @@ -192,7 +197,26 @@ async def _on_command_error(self, ctx: Context, error: Exception) -> None:

# ENTRYPOINT

def start(self) -> None:
def _load_config(self, config: Config | str) -> None:
if self._config is not None:
raise RuntimeError("Config is already loaded.")

if isinstance(config, str):
config = Config(config_path=config)
elif not isinstance(config, Config):
raise TypeError("config must be a Config instance or a config file path")

self._config = config
self._client = AsyncClient(config.homeserver)
self._help = self._help or DefaultHelpCommand()

self.prefix = config.prefix
self.register_command(self.help)

self.client.add_event_callback(self._on_matrix_event, Event)
self._auto_register_events()

def start(self, *, config: Config | str) -> None:
"""
Synchronous entry point for running the bot.

Expand All @@ -201,6 +225,9 @@ def start(self) -> None:
:func:`asyncio.run`, and ensures the client is closed gracefully
on interruption.
"""
if config is not None:
self._load_config(config)

try:
asyncio.run(self.run())
except KeyboardInterrupt:
Expand Down
Loading
Loading