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 argumentsraise 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)}!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):
"""