From fa97e4b098b811ac2ecb55ea5cfe28769e60b9a1 Mon Sep 17 00:00:00 2001 From: nikita Date: Sat, 12 Sep 2026 20:16:39 +0300 Subject: [PATCH] Repo improvements - Improved README.md - Added examples - Added `session_storage/` to .gitignore so that examples do not introduce leftover files after execution --- .gitignore | 1 + README.md | 92 ++++++++++++++++------------------ examples/_environment.py | 18 +++++++ examples/echo_bot.py | 51 +++++++++++++++++++ examples/image_gen_bot.py | 103 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 216 insertions(+), 49 deletions(-) create mode 100644 examples/_environment.py create mode 100644 examples/echo_bot.py create mode 100644 examples/image_gen_bot.py diff --git a/.gitignore b/.gitignore index fd469d4..ee22eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ +session_storage/ *.vscode .venv/ dist/ diff --git a/README.md b/README.md index 8f31667..8f40c09 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,61 @@ -# mab +# 🤖 mab **mab** *(MAtrix Bot)* is a **very** simple Python package that can be used to -develop **very** simple Matrix bots. I have decided to make something like this -because I wasn't satisfied by simplicity and usage of other libraries. So -this library does not aim to be "the best matrix bot library", it only aims to -be good enough for me. +develop **very** simple Matrix bots. It does not aim to be the best library out +there, but it aims to be convenient and usable for relatively serious projects. -## Features +## ✨ Features -The package supports the following features: +The library supports the following features: +- **Completely `asyncio` based** - **Filter-based callback system** -- **Images sending** -- **Videos sending with automatic thumbnail generation (requires `ffmpeg`)** +- **Sending images** +- **Sending videos with automatic thumbnail generation (requires `ffmpeg`)** -## Installation +## 🚀 Usage -Use `pip` to install this package: +Use `apt` to install required system packages and `pip` to install the package. +You may need to use `root` privileges to use `apt`. It's highly recommended you +use `venv` or another Python virtual environment. Here are the commands to +install the latest version of the library: ```bash +apt install libmagic1-dev libolm-dev python -m pip install git+https://git.tyukalov.su/nikita/mab@v0.4.0 ``` -You should specify package version you want to use, because `main` without tags -contains unstable code. +`libmagic1-dev` is needed for automatic file MIME type detection, `libolm-dev` +is needed for E2EE to work. -## Basic usage +Please inspect [`examples/shell_bot.py`](examples/shell_bot.py), +[`examples/echo_bot.py`](examples/echo_bot.py) or open [`examples/`](examples/) +directory to find usage examples. Examples require that you set +`MATRIX_HOMESERVER` and `MATRIX_USERNAME` environment variables. Examples create +`session_storage` directory in working directory. -This is the most simple bot you can create. It would respond to any message -that contains `hello` and `hi` words. +## 🏷️ Versioning -```python -import asyncio -from pathlib import Path -from mab import MatrixBot, MatrixBotConfig -from mab import TextContainsFilter, SenderIsBotFilter -from mab.types import RoomEventData +Releases are tagged in this repository using the `vX.Y.Z` format. If the commit +is not tagged, it must be treated as versionless and should not be used for your +application. +- `X` **(Major)**: Breaking architectiral changes or complete rewrites. Existing + code will break. Note that `0.Y.Z` versions are considered **very unstable**, + the API may change at any time and some features do not work as expected. +- `Y` **(Minor)**: Breaking API changes, feature removals, or behavioral + modifications. Existing code will likely break. +- `Z` **(Patch)**: Backward-compatible feature additions, bug fixes, or internal + changes. Existing code will not break. -async def on_message(data: RoomEventData) -> None: - text = f"Your message contains {len(data.event.body)} symbols" - await data.bot.send_text(data.room, text) +## 🛠️ Development -async def main() -> None: - # create and start the bot - cfg = MatrixBotConfig( - matrix_homeserver_url="matrix.domain.su", - matrix_username_localpart="nagibator666", - storage_directory=Path("storage_nagibator666") - ) - bot = MatrixBot(matrix_bot_config) - bot.add_callback( - ~SenderIsBotFilter() & TextContainsFilter(["hello", "hi"]), - on_message - ) - await bot.start() - # wait for Ctrl+C - try: - while True: - await asyncio.sleep(1) - except: - pass - # stop the bot - await bot.stop() - -if __name__ == "__main__": - asyncio.run(main()) +Here's the list of commands you should execute to get started with development +(including cloning the repository and installing required packages). Please note +that your workflow may use something other than `venv`. +```bash +apt install libmagic1-dev libolm-dev +git clone https://git.tyukalov.su/nikita/mab +cd mab +python3 -m venv .venv +. .venv/bin/activate +pip install -e . ``` \ No newline at end of file diff --git a/examples/_environment.py b/examples/_environment.py new file mode 100644 index 0000000..75fa6dd --- /dev/null +++ b/examples/_environment.py @@ -0,0 +1,18 @@ +import os +import sys + +def check_environment() -> None: + """ + This function checks if required environment variables are set. It prints + problem resolution guide and exits using `sys.exit(1)` on problem. + """ + if "MATRIX_HOMESERVER" not in os.environ: + print("Please set `MATRIX_HOMESERVER` environment variable!") + print("P.S. use something like this in your shell:") + print(" export MATRIX_HOMESERVER=\"https://matrix.server.net\"") + sys.exit(1) + if "MATRIX_USERNAME" not in os.environ: + print("Please set `MATRIX_USERNAME` environment variable!") + print("P.S. use something like this in your shell:") + print(" export MATRIX_USERNAME=\"megakiller228\"") + sys.exit(1) \ No newline at end of file diff --git a/examples/echo_bot.py b/examples/echo_bot.py new file mode 100644 index 0000000..3734ea4 --- /dev/null +++ b/examples/echo_bot.py @@ -0,0 +1,51 @@ +""" +This example implements Matrix bot that echoes all text messages it receives. + +It uses environment variables to specify authorization data. Use Ctrl+C to stop +the bot. +""" + +import asyncio +import os +import logging + +from mab import ( + MatrixBot, + MatrixBotConfig, + RoomEventData, + BodyExistsFilter, + MessageTypeFilter, + SenderIsBotFilter, + MessageType +) + +from _environment import check_environment + +async def on_text_message(data: RoomEventData) -> None: + """This callback is called when a text message arrives.""" + await data.bot.send_text(data.room, data.event.body) # type: ignore + +async def main() -> None: + """Application entry point""" + logging.basicConfig(level=logging.INFO) + logging.getLogger("nio").setLevel(logging.CRITICAL+1) + check_environment() + + config = MatrixBotConfig( + matrix_homeserver_url=os.environ["MATRIX_HOMESERVER"], + matrix_username_localpart=os.environ["MATRIX_USERNAME"], + storage_directory="session_storage" + ) + bot = MatrixBot(config) + bot.add_callback( + ~SenderIsBotFilter() & BodyExistsFilter() & MessageTypeFilter(MessageType.TEXT), + on_text_message) + + # run until Ctrl+C + try: + await bot.run() + except asyncio.CancelledError: + pass + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/image_gen_bot.py b/examples/image_gen_bot.py new file mode 100644 index 0000000..7a85830 --- /dev/null +++ b/examples/image_gen_bot.py @@ -0,0 +1,103 @@ +""" +This example implements Matrix bot that generates a pixelized noise image with +specified maximum R, G and B values. + +It uses environment variables to specify authorization data. Use Ctrl+C to stop +the bot. +""" + +import asyncio +from io import BytesIO +import os +import logging +import random +from PIL import Image + +from mab import ( + MatrixBot, + MatrixBotConfig, + RoomEventData, + MessageTypeFilter, + BodyCommandFilter, + SenderIsBotFilter, + MessageType +) + +from _environment import check_environment + +async def on_gen_command(data: RoomEventData) -> None: + """This callback is called when `!gen R G B` command is received.""" + # convert R, G and B to floats + try: + r, g, b = [float(v) for v in data.event.command_args] # type: ignore + except: + await data.bot.send_text(data.room, "Invalid arguments") + return + await data.bot.send_text(data.room, "Generating the noise...") + # create the basic noise + img = Image.new("RGB", (16, 16)) + for x in range(img.width): + for y in range(img.height): + col = (random.random() * r, random.random() * g, random.random() * b) + img.putpixel( + (x, y), + tuple(int(c * 255) for c in col) + ) + # pixelized upscale + img = img.resize((2048, 2048), resample=Image.Resampling.NEAREST) + # save to buffer + buf = BytesIO() + img.save(buf, format="PNG") + buf.seek(0) + buf = buf.read() + # send + await data.bot.send_image_bytes(data.room, buf, "noise.png") + +async def on_wrong_message(data: RoomEventData) -> None: + """This callback is called when a wrong message is received.""" + await data.bot.send_text( + data.room, + "Text me something like !gen 0.1 0.7 1.0" + ) + +async def main() -> None: + """Application entry point""" + logging.basicConfig(level=logging.INFO) + logging.getLogger("nio").setLevel(logging.CRITICAL+1) + check_environment() + + config = MatrixBotConfig( + matrix_homeserver_url=os.environ["MATRIX_HOMESERVER"], + matrix_username_localpart=os.environ["MATRIX_USERNAME"], + storage_directory="session_storage" + ) + bot = MatrixBot(config) + command_filter = MessageTypeFilter(MessageType.TEXT) & BodyCommandFilter( + verbs=["gen"], + min_args=3, + max_args=3 + ) + + # callback for message that + # 1. are sent not by this bot + # 2. do match the command filter + bot.add_callback( + ~SenderIsBotFilter() & command_filter, + on_gen_command) + + # callback for message that + # 1. are sent not by this bot + # 2. do NOT match the command filter + bot.add_callback( + ~SenderIsBotFilter() & ~command_filter, + on_wrong_message + ) + + # run until Ctrl+C + try: + await bot.run() + except asyncio.CancelledError: + pass + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file