From 2867582c01512a6087c190f70949572c21268f33 Mon Sep 17 00:00:00 2001 From: nikita Date: Sun, 27 Sep 2026 16:17:40 +0300 Subject: [PATCH] Got rid of proxy methods in `MatrixBot`, v0.6.0 - Updated README.md - Updated examples - `MatrixBot` does not redefine methods from classes it is composed of anymore --- README.md | 3 +- examples/command_bot.py | 18 +-- examples/echo_bot.py | 2 +- examples/file_bot.py | 14 +- examples/image_bot.py | 12 +- pyproject.toml | 2 +- src/mab/bot/_client_downloader.py | 15 +- src/mab/bot/_client_sender.py | 112 +++++++------- src/mab/bot/bot.py | 238 ++---------------------------- src/mab/filters/message.py | 2 +- 10 files changed, 107 insertions(+), 311 deletions(-) diff --git a/README.md b/README.md index 4247986..ba555f7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The library supports the following features: - **Completely `asyncio` based** - **Filter-based callback system** - **Downloading and transparently decrypting files** +- **Sending files** - **Sending images** - **Sending videos with automatic thumbnail generation (requires `ffmpeg`)** @@ -22,7 +23,7 @@ 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.5.1 +python -m pip install git+https://git.tyukalov.su/nikita/mab@v0.6.0 ``` `libmagic1-dev` is needed for automatic file MIME type detection, `libolm-dev` diff --git a/examples/command_bot.py b/examples/command_bot.py index 4f9da59..f960def 100644 --- a/examples/command_bot.py +++ b/examples/command_bot.py @@ -29,18 +29,18 @@ async def on_help_command(ctx: EventContext) -> None: "
  • !args arg1 [arg2] ... [arg5] - command that takes 1..5 arguments
  • " "" ) - await ctx.bot.send_text(ctx.room, HELP_MESSAGE) + await ctx.bot.send.text(ctx.room, HELP_MESSAGE) async def on_time_command(ctx: EventContext) -> None: """!time""" - await ctx.bot.send_text( + await ctx.bot.send.text( ctx.room, f"Current UNIX timestamp is {int(time.time())}" ) async def on_raise_command(ctx: EventContext) -> None: """!raise""" - await ctx.bot.send_text( + await ctx.bot.send.text( ctx.room, "Executing raise RuntimeError()..." ) @@ -48,7 +48,7 @@ async def on_raise_command(ctx: EventContext) -> None: async def on_assert_command(ctx: EventContext) -> None: """!assert""" - await ctx.bot.send_text( + await ctx.bot.send.text( ctx.room, "Executing assert False..." ) @@ -63,9 +63,9 @@ async def on_mul_command(ctx: EventContext) -> None: v *= n response = " * ".join(html.escape("%.2f" % n) for n in numbers) response += f" = {html.escape(str(v))}" - await ctx.bot.send_text(ctx.room, response) + await ctx.bot.send.text(ctx.room, response) except Exception as e: - await ctx.bot.send_text(ctx.room, f"Could not process the command: {e}") + await ctx.bot.send.text(ctx.room, f"Could not process the command: {e}") async def on_args_command(ctx: EventContext) -> None: """!args""" @@ -79,19 +79,19 @@ async def on_args_command(ctx: EventContext) -> None: for arg in ctx[CTX_CMD_ARGS]: response += f"
  • {html.escape(arg)}
  • " response += "" - await ctx.bot.send_text( + await ctx.bot.send.text( ctx.room, response ) except: - await ctx.bot.send_text( + await ctx.bot.send.text( ctx.room, f"Could not process the command: {traceback.format_exc()}" ) async def invalid_usage(ctx: EventContext) -> None: """This callback is called when the bot used incorrectly.""" - await ctx.bot.send_text(ctx.room, "Use !help") + await ctx.bot.send.text(ctx.room, "Use !help") async def main() -> None: diff --git a/examples/echo_bot.py b/examples/echo_bot.py index 3a2b075..4a71846 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -15,7 +15,7 @@ from _environment import check_environment async def on_text_message(ctx: EventContext) -> None: """This callback is called when a text message arrives.""" - await ctx.bot.send_text(ctx.room, ctx[CTX_BODY]) + await ctx.bot.send.text(ctx.room, ctx[CTX_BODY]) async def main() -> None: """Application entry point""" diff --git a/examples/file_bot.py b/examples/file_bot.py index ef64292..ce140f3 100644 --- a/examples/file_bot.py +++ b/examples/file_bot.py @@ -18,18 +18,18 @@ from _environment import check_environment async def on_text_message(ctx: EventContext) -> None: """This callback is called when a text message arrives.""" - await ctx.bot.send_text(ctx.room, "Please send a file/image/video") + await ctx.bot.send.text(ctx.room, "Please send a file/image/video") async def on_file_message(ctx: EventContext) -> None: """This callback is called when a file arrives.""" sha = hashlib.sha256() does_temp_exist = False if ctx[CTX_FILE_SIZE] > 1_000_000: - await ctx.bot.send_text( + await ctx.bot.send.text( ctx.room, "The file is larger than 1 MB, downloading to filesystem" ) - await ctx.bot.download_file(ctx, path="temp.tmp") + await ctx.bot.download.file(ctx, path="temp.tmp") async with aiofiles.open("temp.tmp", "rb") as f: while True: data = await f.read(64 * 1024) @@ -38,16 +38,16 @@ async def on_file_message(ctx: EventContext) -> None: sha.update(data) does_temp_exist = True else: - await ctx.bot.send_text( + await ctx.bot.send.text( ctx.room, "The file is smaller than 1 MB, downloading to RAM" ) - content = await ctx.bot.download_file(ctx, path=None) + content = await ctx.bot.download.file(ctx, path=None) sha.update(content) # result response = f"SHA256 for file `{ctx[CTX_FILE_NAME]}`" response += f" ({ctx[CTX_FILE_SIZE]} bytes, {ctx[CTX_FILE_MIME]})" - await ctx.bot.send_file_bytes( + await ctx.bot.send.file_bytes( room=ctx.room, data=sha.hexdigest().encode("utf-8"), filename="hash of the file.txt", @@ -56,7 +56,7 @@ async def on_file_message(ctx: EventContext) -> None: ) # resend the file to test uploading if does_temp_exist: - await ctx.bot.send_file( + await ctx.bot.send.file( room=ctx.room, path="temp.tmp", filename=ctx[CTX_FILE_NAME], diff --git a/examples/image_bot.py b/examples/image_bot.py index 48816c8..4b1efa7 100644 --- a/examples/image_bot.py +++ b/examples/image_bot.py @@ -24,9 +24,9 @@ async def on_gen_command(data: EventContext) -> None: try: r, g, b = [float(v) for v in data[CTX_CMD_ARGS]] except: - await data.bot.send_text(data.room, "Invalid arguments") + await data.bot.send.text(data.room, "Invalid arguments") return - await data.bot.send_text(data.room, "Generating the noise...") + 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): @@ -44,13 +44,13 @@ async def on_gen_command(data: EventContext) -> None: buf.seek(0) buf = buf.read() # send - await data.bot.send_image_bytes(data.room, buf, "noise.png") + await data.bot.send.image_bytes(data.room, buf, "noise.png") async def on_image(ctx: EventContext) -> None: """This callback is called when an image is received.""" # download s = time.time() - data = await ctx.bot.download_file(ctx) + data = await ctx.bot.download.file(ctx) took_time = time.time() - s # convert to Image with BytesIO(data) as buf: @@ -66,7 +66,7 @@ async def on_image(ctx: EventContext) -> None: buf.seek(0) buf = buf.read() # send - await ctx.bot.send_image_bytes( + await ctx.bot.send.image_bytes( ctx.room, buf, "blurred.png", @@ -75,7 +75,7 @@ async def on_image(ctx: EventContext) -> None: async def on_wrong_message(data: EventContext) -> None: """This callback is called when a wrong message is received.""" - await data.bot.send_text( + await data.bot.send.text( data.room, "Text me something like !gen 0.1 0.7 1.0 or send an image to blur" ) diff --git a/pyproject.toml b/pyproject.toml index 39d88cc..b44f806 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mab" -version = "0.5.2" +version = "0.6.0" authors = [ { name = "Tyukalov Nikita", email = "nikita@tyukalov.su" } ] diff --git a/src/mab/bot/_client_downloader.py b/src/mab/bot/_client_downloader.py index ad0a6e5..d8d61b7 100644 --- a/src/mab/bot/_client_downloader.py +++ b/src/mab/bot/_client_downloader.py @@ -2,6 +2,7 @@ import asyncio import aiofiles import logging import os +from typing import overload import threading from uuid import uuid4 from pathlib import Path @@ -149,7 +150,19 @@ class ClientDownloader: cancel.set() raise - async def download_file(self, + @overload + async def file(self, + source: EventContext | Event, + *, + path: str | Path) -> Path: ... + + @overload + async def file(self, + source: EventContext | Event, + *, + path: None = None) -> bytes: ... + + async def file(self, source: EventContext | Event, *, path: str | Path | None = None) -> Path | bytes: diff --git a/src/mab/bot/_client_sender.py b/src/mab/bot/_client_sender.py index fedfeb0..73713f3 100644 --- a/src/mab/bot/_client_sender.py +++ b/src/mab/bot/_client_sender.py @@ -98,9 +98,9 @@ class ClientSender: self._client = client self._uploader = uploader - async def send_content(self, - room: MatrixRoom | str, - content: dict) -> RoomSendResponse: + async def content(self, + room: MatrixRoom | str, + content: dict) -> RoomSendResponse: """ Send raw `content` JSON to `room`. @@ -124,7 +124,7 @@ class ClientSender: if self._config.auto_verify_all_known_devices: if not Utils.verify_all_known_devices(self._client): raise - return await self.send_content(room, content) + return await self.content(room, content) else: raise if type(result) is RoomSendResponse: @@ -134,11 +134,11 @@ class ClientSender: else: raise RuntimeError("Unknown error has occured", result) - async def send_text(self, - room: MatrixRoom | str, - text: str, - *, - is_html: bool | None = None) -> str: + async def text(self, + room: MatrixRoom | str, + text: str, + *, + is_html: bool | None = None) -> str: """ Send text message to `room`. @@ -156,15 +156,15 @@ class ClientSender: "msgtype": "m.text", **text_data } - return (await self.send_content(room, content)).event_id + return (await self.content(room, content)).event_id - async def send_image(self, - room: MatrixRoom | str, - path: Path | str, *, - text: str | None = None, - is_html: bool | None = None, - filename: str | None = None, - timeout: float | None = 60 * 60) -> str: + async def image(self, + room: MatrixRoom | str, + path: Path | str, *, + text: str | None = None, + is_html: bool | None = None, + filename: str | None = None, + timeout: float | None = 60 * 60) -> str: """ Send the image to `room`. Please note that formatted text is displayed incorrectly in some clients as of September 8th, 2026 @@ -213,16 +213,16 @@ class ClientSender: "h": height } } - return (await self.send_content(room, content)).event_id + return (await self.content(room, content)).event_id - async def send_image_bytes(self, - room: MatrixRoom | str, - data: bytes, - filename: str, - *, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60) -> str: + async def image_bytes(self, + room: MatrixRoom | str, + data: bytes, + filename: str, + *, + text: str | None = None, + is_html: bool | None = None, + timeout: float | None = 60 * 60) -> str: """ Send the image to `room`. Please note that formatted text is displayed incorrectly in some clients as of September 8th, 2026 @@ -276,16 +276,16 @@ class ClientSender: "h": height } } - return (await self.send_content(room, content)).event_id + return (await self.content(room, content)).event_id - async def send_video(self, - room: MatrixRoom | str, - path: Path | str, - *, - props: VideoFileProperties | None = None, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60) -> str: + async def video(self, + room: MatrixRoom | str, + path: Path | str, + *, + props: VideoFileProperties | None = None, + text: str | None = None, + is_html: bool | None = None, + timeout: float | None = 60 * 60) -> str: """ Send the video to `room`. Please note that formatted text is displayed incorrectly in some clients as of September 8th, 2026. Unknown video @@ -361,17 +361,17 @@ class ClientSender: } } # send - return (await self.send_content(room, content)).event_id + return (await self.content(room, content)).event_id - async def send_file(self, - room: MatrixRoom | str, - path: Path | str, - *, - filename: str | None = None, - mime_type: str | None = None, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60): + async def file(self, + room: MatrixRoom | str, + path: Path | str, + *, + filename: str | None = None, + mime_type: str | None = None, + text: str | None = None, + is_html: bool | None = None, + timeout: float | None = 60 * 60): """ Send the file to `room`. Please note that formatted text is displayed incorrectly in some clients as of September 8th, 2026. @@ -422,17 +422,17 @@ class ClientSender: "size": upload_result.filesize } } - return (await self.send_content(room, content)).event_id + return (await self.content(room, content)).event_id - async def send_file_bytes(self, - room: MatrixRoom | str, - data: bytes, - filename: str, - *, - mime_type: str | None = None, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60) -> str: + async def file_bytes(self, + room: MatrixRoom | str, + data: bytes, + filename: str, + *, + mime_type: str | None = None, + text: str | None = None, + is_html: bool | None = None, + timeout: float | None = 60 * 60) -> str: """ Send the file to `room`. @@ -480,4 +480,4 @@ class ClientSender: "size": upload_result.filesize } } - return (await self.send_content(room, content)).event_id \ No newline at end of file + return (await self.content(room, content)).event_id \ No newline at end of file diff --git a/src/mab/bot/bot.py b/src/mab/bot/bot.py index 3832aa9..5fb086d 100644 --- a/src/mab/bot/bot.py +++ b/src/mab/bot/bot.py @@ -119,7 +119,8 @@ class MatrixBot: finally: await self.stop() - def get_client(self) -> AsyncClient: + @property + def client(self) -> AsyncClient: """ Get AsyncClient. @@ -129,231 +130,12 @@ class MatrixBot: """ return self._client_manager.get_client() - async def send_text(self, - room: MatrixRoom | str, - text: str, - *, - is_html: bool | None = None) -> str: - """ - Send text message to `room`. + @property + def send(self) -> ClientSender: + """Get ClientSender that you should use to send messages.""" + return self._client_sender - Args: - - room - the room to send the text to - - text - the text to send to the room - - is_html - whether the text is HTML-formatted. Use `None` for auto - - Returns: - - `event_id` of sent message on success - - Raises an exception on error - """ - return await self._client_sender.send_text( - room=room, - text=text, - is_html=is_html - ) - - async def send_image(self, - room: MatrixRoom | str, - path: Path | str, *, - text: str | None = None, - is_html: bool | None = None, - filename: str | None = None, - timeout: float | None = 60 * 60) -> str: - """ - Send the image to `room`. Please note that formatted text is displayed - incorrectly in some clients as of September 8th, 2026 - - Args: - - room - the room to send the text to - - path - path to the image file - - text - image caption to use (`None` to disable) - - is_html - whether the text is HTML-formatted (`None` for auto) - - filename - filename to use for the file (`None` for auto) - - timeout - upload timeout in seconds (`None` to disable) - - Returns: - - `event_id` of sent message on success - - Raises an exception on error - """ - return await self._client_sender.send_image( - room=room, - path=path, - text=text, - is_html=is_html, - filename=filename, - timeout=timeout - ) - - async def send_image_bytes(self, - room: MatrixRoom | str, - data: bytes, - filename: str, - *, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60) -> str: - """ - Send the image to `room`. Please note that formatted text is displayed - incorrectly in some clients as of September 8th, 2026 - - Args: - - room - the room to send the text to - - bytes - the image to send - - filename - filename to use for the file - - text - image caption to use (`None` to disable) - - is_html - whether the text is HTML-formatted (`None` for auto) - - timeout - upload timeout in seconds (`None` to disable) - - Returns: - - `event_id` of sent message on success - - Raises an exception on error - """ - return await self._client_sender.send_image_bytes( - room=room, - data=data, - filename=filename, - text=text, - is_html=is_html, - timeout=timeout - ) - - async def send_video(self, - room: MatrixRoom | str, - path: Path | str, - *, - props: VideoFileProperties | None = None, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60) -> str: - """ - Send the video to `room`. Please note that formatted text is displayed - incorrectly in some clients as of September 8th, 2026. Unknown video - properties will be automatically deduced as configured in - `MatrixBotConfig`. - - Args: - - room - the room to send the text to - - path - path to the video file - - props - video properties (`None` for auto, if the feature is ON) - - text - video caption to use (`None` to disable) - - is_html - whether the text is HTML-formatted (`None` for auto) - - timeout - upload timeout in seconds (`None` to disable) - - Returns: - - `event_id` of sent message on success - - Raises an exception on error - """ - return await self._client_sender.send_video( - room=room, - path=path, - props=props, - text=text, - is_html=is_html, - timeout=timeout - ) - - async def send_file(self, - room: MatrixRoom | str, - path: Path | str, - *, - filename: str | None = None, - mime_type: str | None = None, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60): - """ - Send the file to `room`. Please note that formatted text is displayed - incorrectly in some clients as of September 8th, 2026. - - Args: - - room - the room to send the file to - - path - path to the file - - filename - filename to use for upload (`None` to use basename from - `path`) - - mime_type - mime type to use (`None` for auto) - - text - caption to use (`None` to disable) - - is_html - whether the text is HTML-formatted (`None` for auto) - - timeout - upload timeout in seconds (`None` to disable) - - Returns: - - `event_id` of sent message on success - - Raises an exception on error - """ - return await self._client_sender.send_file( - room=room, - path=path, - filename=filename, - mime_type=mime_type, - text=text, - is_html=is_html, - timeout=timeout - ) - - async def send_file_bytes(self, - room: MatrixRoom | str, - data: bytes, - filename: str, - *, - mime_type: str | None = None, - text: str | None = None, - is_html: bool | None = None, - timeout: float | None = 60 * 60) -> str: - """ - Send the file to `room`. - - Args: - - room - the room to send the file to - - data - the content of the file to send - - filename - filename to use for the file - - text - caption to use (`None` to disable) - - is_html - whether the text is HTML-formatted (`None` for auto) - - timeout - upload timeout in seconds (`None` to disable) - - Returns: - - `event_id` of sent message on success - - Raises an exception on error - """ - return await self._client_sender.send_file_bytes( - room=room, - data=data, - filename=filename, - mime_type=mime_type, - text=text, - is_html=is_html, - timeout=timeout - ) - - @overload - async def download_file(self, - source: EventContext | Event, - *, - path: str | Path) -> Path: ... - - @overload - async def download_file(self, - source: EventContext | Event, - *, - path: None = None) -> bytes: ... - - async def download_file(self, - source: EventContext | Event, - *, - path: str | Path | None = None) -> Path | bytes: - """ - Download the file from the event. Automatically dechiphers encrypted - media. - - Args: - - source - event that has a file in its `content` - - path - where to save the file to. Use `None` to store the file in - memory. Use path to specify file download path. - - Returns: - - Returns `Path` to the file on success (if `path` isn't `None`) - - Returns `bytes` of the file on success (if `path` is `None`) - - Raises an exception on error - """ - return await self._client_downloader.download_file( - source=source, - path=path - ) \ No newline at end of file + @property + def download(self) -> ClientDownloader: + """Get ClientDownloader that you should use to download files.""" + return self._client_downloader \ No newline at end of file diff --git a/src/mab/filters/message.py b/src/mab/filters/message.py index 2c4a770..ab2498b 100644 --- a/src/mab/filters/message.py +++ b/src/mab/filters/message.py @@ -123,7 +123,7 @@ class SenderIsBotFilter(BaseEventFilter): async def __call__(self, context: EventContext) -> bool: if not await super().__call__(context): return False - return context.bot.get_client().user_id == context.event.sender + return context.bot.client.user_id == context.event.sender class MessageHasFile(BaseEventFilter): """