2 Commits

Author SHA1 Message Date
b22e7b4903 Added room operations (not tested) 2026-09-27 16:47:08 +03:00
2867582c01 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
2026-09-27 16:17:40 +03:00
11 changed files with 265 additions and 309 deletions

View File

@@ -10,6 +10,7 @@ The library supports the following features:
- **Completely `asyncio` based** - **Completely `asyncio` based**
- **Filter-based callback system** - **Filter-based callback system**
- **Downloading and transparently decrypting files** - **Downloading and transparently decrypting files**
- **Sending files**
- **Sending images** - **Sending images**
- **Sending videos with automatic thumbnail generation (requires `ffmpeg`)** - **Sending videos with automatic thumbnail generation (requires `ffmpeg`)**
@@ -22,7 +23,7 @@ install the latest version of the library:
```bash ```bash
apt install libmagic1-dev libolm-dev 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.1
``` ```
`libmagic1-dev` is needed for automatic file MIME type detection, `libolm-dev` `libmagic1-dev` is needed for automatic file MIME type detection, `libolm-dev`

View File

@@ -29,18 +29,18 @@ async def on_help_command(ctx: EventContext) -> None:
"<li><code>!args arg1 [arg2] ... [arg5]</code> - command that takes 1..5 arguments</li>" "<li><code>!args arg1 [arg2] ... [arg5]</code> - command that takes 1..5 arguments</li>"
"</ul>" "</ul>"
) )
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: async def on_time_command(ctx: EventContext) -> None:
"""!time""" """!time"""
await ctx.bot.send_text( await ctx.bot.send.text(
ctx.room, ctx.room,
f"Current UNIX timestamp is <strong>{int(time.time())}</strong>" f"Current UNIX timestamp is <strong>{int(time.time())}</strong>"
) )
async def on_raise_command(ctx: EventContext) -> None: async def on_raise_command(ctx: EventContext) -> None:
"""!raise""" """!raise"""
await ctx.bot.send_text( await ctx.bot.send.text(
ctx.room, ctx.room,
"<strong>Executing <code>raise RuntimeError()</code>...</strong>" "<strong>Executing <code>raise RuntimeError()</code>...</strong>"
) )
@@ -48,7 +48,7 @@ async def on_raise_command(ctx: EventContext) -> None:
async def on_assert_command(ctx: EventContext) -> None: async def on_assert_command(ctx: EventContext) -> None:
"""!assert""" """!assert"""
await ctx.bot.send_text( await ctx.bot.send.text(
ctx.room, ctx.room,
"<strong>Executing <code>assert False</code>...</strong>" "<strong>Executing <code>assert False</code>...</strong>"
) )
@@ -63,9 +63,9 @@ async def on_mul_command(ctx: EventContext) -> None:
v *= n v *= n
response = " * ".join(html.escape("%.2f" % n) for n in numbers) response = " * ".join(html.escape("%.2f" % n) for n in numbers)
response += f" = <strong>{html.escape(str(v))}<strong>" response += f" = <strong>{html.escape(str(v))}<strong>"
await ctx.bot.send_text(ctx.room, response) await ctx.bot.send.text(ctx.room, response)
except Exception as e: 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: async def on_args_command(ctx: EventContext) -> None:
"""!args""" """!args"""
@@ -79,19 +79,19 @@ async def on_args_command(ctx: EventContext) -> None:
for arg in ctx[CTX_CMD_ARGS]: for arg in ctx[CTX_CMD_ARGS]:
response += f"<li><code>{html.escape(arg)}</code></li>" response += f"<li><code>{html.escape(arg)}</code></li>"
response += "</ol>" response += "</ol>"
await ctx.bot.send_text( await ctx.bot.send.text(
ctx.room, ctx.room,
response response
) )
except: except:
await ctx.bot.send_text( await ctx.bot.send.text(
ctx.room, ctx.room,
f"Could not process the command: {traceback.format_exc()}" f"Could not process the command: {traceback.format_exc()}"
) )
async def invalid_usage(ctx: EventContext) -> None: async def invalid_usage(ctx: EventContext) -> None:
"""This callback is called when the bot used incorrectly.""" """This callback is called when the bot used incorrectly."""
await ctx.bot.send_text(ctx.room, "Use <code>!help</code>") await ctx.bot.send.text(ctx.room, "Use <code>!help</code>")
async def main() -> None: async def main() -> None:

View File

@@ -15,7 +15,7 @@ from _environment import check_environment
async def on_text_message(ctx: EventContext) -> None: async def on_text_message(ctx: EventContext) -> None:
"""This callback is called when a text message arrives.""" """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: async def main() -> None:
"""Application entry point""" """Application entry point"""

View File

@@ -18,18 +18,18 @@ from _environment import check_environment
async def on_text_message(ctx: EventContext) -> None: async def on_text_message(ctx: EventContext) -> None:
"""This callback is called when a text message arrives.""" """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: async def on_file_message(ctx: EventContext) -> None:
"""This callback is called when a file arrives.""" """This callback is called when a file arrives."""
sha = hashlib.sha256() sha = hashlib.sha256()
does_temp_exist = False does_temp_exist = False
if ctx[CTX_FILE_SIZE] > 1_000_000: if ctx[CTX_FILE_SIZE] > 1_000_000:
await ctx.bot.send_text( await ctx.bot.send.text(
ctx.room, ctx.room,
"The file is larger than 1 MB, downloading to filesystem" "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: async with aiofiles.open("temp.tmp", "rb") as f:
while True: while True:
data = await f.read(64 * 1024) data = await f.read(64 * 1024)
@@ -38,16 +38,16 @@ async def on_file_message(ctx: EventContext) -> None:
sha.update(data) sha.update(data)
does_temp_exist = True does_temp_exist = True
else: else:
await ctx.bot.send_text( await ctx.bot.send.text(
ctx.room, ctx.room,
"The file is smaller than 1 MB, downloading to RAM" "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) sha.update(content)
# result # result
response = f"SHA256 for file `{ctx[CTX_FILE_NAME]}`" response = f"SHA256 for file `{ctx[CTX_FILE_NAME]}`"
response += f" ({ctx[CTX_FILE_SIZE]} bytes, {ctx[CTX_FILE_MIME]})" 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, room=ctx.room,
data=sha.hexdigest().encode("utf-8"), data=sha.hexdigest().encode("utf-8"),
filename="hash of the file.txt", filename="hash of the file.txt",
@@ -56,7 +56,7 @@ async def on_file_message(ctx: EventContext) -> None:
) )
# resend the file to test uploading # resend the file to test uploading
if does_temp_exist: if does_temp_exist:
await ctx.bot.send_file( await ctx.bot.send.file(
room=ctx.room, room=ctx.room,
path="temp.tmp", path="temp.tmp",
filename=ctx[CTX_FILE_NAME], filename=ctx[CTX_FILE_NAME],

View File

@@ -24,9 +24,9 @@ async def on_gen_command(data: EventContext) -> None:
try: try:
r, g, b = [float(v) for v in data[CTX_CMD_ARGS]] r, g, b = [float(v) for v in data[CTX_CMD_ARGS]]
except: except:
await data.bot.send_text(data.room, "Invalid arguments") await data.bot.send.text(data.room, "Invalid arguments")
return 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 # create the basic noise
img = Image.new("RGB", (16, 16)) img = Image.new("RGB", (16, 16))
for x in range(img.width): for x in range(img.width):
@@ -44,13 +44,13 @@ async def on_gen_command(data: EventContext) -> None:
buf.seek(0) buf.seek(0)
buf = buf.read() buf = buf.read()
# send # 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: async def on_image(ctx: EventContext) -> None:
"""This callback is called when an image is received.""" """This callback is called when an image is received."""
# download # download
s = time.time() s = time.time()
data = await ctx.bot.download_file(ctx) data = await ctx.bot.download.file(ctx)
took_time = time.time() - s took_time = time.time() - s
# convert to Image # convert to Image
with BytesIO(data) as buf: with BytesIO(data) as buf:
@@ -66,7 +66,7 @@ async def on_image(ctx: EventContext) -> None:
buf.seek(0) buf.seek(0)
buf = buf.read() buf = buf.read()
# send # send
await ctx.bot.send_image_bytes( await ctx.bot.send.image_bytes(
ctx.room, ctx.room,
buf, buf,
"blurred.png", "blurred.png",
@@ -75,7 +75,7 @@ async def on_image(ctx: EventContext) -> None:
async def on_wrong_message(data: EventContext) -> None: async def on_wrong_message(data: EventContext) -> None:
"""This callback is called when a wrong message is received.""" """This callback is called when a wrong message is received."""
await data.bot.send_text( await data.bot.send.text(
data.room, data.room,
"Text me something like <code>!gen 0.1 0.7 1.0</code> or send an image to blur" "Text me something like <code>!gen 0.1 0.7 1.0</code> or send an image to blur"
) )

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "mab" name = "mab"
version = "0.5.2" version = "0.6.1"
authors = [ authors = [
{ name = "Tyukalov Nikita", email = "nikita@tyukalov.su" } { name = "Tyukalov Nikita", email = "nikita@tyukalov.su" }
] ]

View File

@@ -2,6 +2,7 @@ import asyncio
import aiofiles import aiofiles
import logging import logging
import os import os
from typing import overload
import threading import threading
from uuid import uuid4 from uuid import uuid4
from pathlib import Path from pathlib import Path
@@ -149,7 +150,19 @@ class ClientDownloader:
cancel.set() cancel.set()
raise 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, source: EventContext | Event,
*, *,
path: str | Path | None = None) -> Path | bytes: path: str | Path | None = None) -> Path | bytes:

View File

@@ -0,0 +1,147 @@
import logging
from nio import (
AsyncClient,
Event,
MatrixRoom,
MatrixUser,
RoomMember,
RoomGetEventResponse,
RoomLeaveResponse,
JoinResponse,
RoomInviteResponse,
RoomKickResponse,
RoomBanResponse,
RoomUnbanResponse,
JoinedMembersResponse
)
from ..utils import MatrixBotConfig
class ClientRoomOperations:
"""This class works with rooms."""
@staticmethod
def _room_id(room: MatrixRoom | str) -> str:
"""Convert room to its ID if needed"""
return room.room_id if isinstance(room, MatrixRoom) else room
@staticmethod
def _user_id(user: MatrixUser | str) -> str:
"""Convert user to its ID if needed"""
return user.user_id if isinstance(user, MatrixUser) else user
def __init__(self):
self._logger = logging.getLogger("ClientRoomOperations")
self._config: MatrixBotConfig | None = None
self._client: AsyncClient | None = None
async def setup(self,
config: MatrixBotConfig,
client: AsyncClient) -> None:
"""Setup room operations.
Args:
- config - config to use
- client - client to use
"""
self._config = config
self._client = client
async def event(self, room: MatrixRoom | str, event_id: str) -> Event:
"""Get raw event information from matrix server (no cache used yet).
Args:
- room - room where the event happened
- event_id - ID of the event
Returns:
- `nio.Event` on success
- raises an exception on matrix error
"""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.room_get_event(
self._room_id(room),
event_id
)
if isinstance(res, RoomGetEventResponse):
return res.event
else:
raise RuntimeError(res)
async def leave(self, room: MatrixRoom | str) -> None:
"""Leave the room (or reject the invite)."""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.room_leave(self._room_id(room))
if not isinstance(res, RoomLeaveResponse):
raise RuntimeError(res)
async def join(self, room: MatrixRoom | str) -> None:
"""Join the room."""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.join(
self._room_id(room)
)
if not isinstance(res, JoinResponse):
raise RuntimeError(res)
async def invite(self, room: MatrixRoom | str, user: MatrixUser | str) -> None:
"""Invite the user to the room."""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.room_invite(
self._room_id(room),
self._user_id(user)
)
if not isinstance(res, RoomInviteResponse):
raise RuntimeError(res)
async def kick(self, room: MatrixRoom | str, user: MatrixUser | str, reason: str | None = None) -> None:
"""Kick the user from the room. The user will be able to join the room
again.
"""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.room_kick(
self._room_id(room),
self._user_id(user),
reason
)
if not isinstance(res, RoomKickResponse):
raise RuntimeError(res)
async def ban(self, room: MatrixRoom | str, user: MatrixUser | str, reason: str | None = None) -> None:
"""Ban the user from the room. The user will be unable to join until he
is unbanned.
"""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.room_ban(
self._room_id(room),
self._user_id(user),
reason
)
if not isinstance(res, RoomBanResponse):
raise RuntimeError(res)
async def unban(self, room: MatrixRoom | str, user: MatrixUser | str) -> None:
"""Unban the user from the room."""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.room_unban(
self._room_id(room),
self._user_id(user)
)
if not isinstance(res, RoomUnbanResponse):
raise RuntimeError(res)
async def members(self, room: MatrixRoom | str) -> list[RoomMember]:
"""Get list of room members."""
if self._config is None or self._client is None:
raise RuntimeError("ClientRoom is not set up")
res = await self._client.joined_members(self._room_id(room))
if not isinstance(res, JoinedMembersResponse):
raise RuntimeError(res)
return res.members

View File

@@ -98,9 +98,9 @@ class ClientSender:
self._client = client self._client = client
self._uploader = uploader self._uploader = uploader
async def send_content(self, async def content(self,
room: MatrixRoom | str, room: MatrixRoom | str,
content: dict) -> RoomSendResponse: content: dict) -> RoomSendResponse:
""" """
Send raw `content` JSON to `room`. Send raw `content` JSON to `room`.
@@ -124,7 +124,7 @@ class ClientSender:
if self._config.auto_verify_all_known_devices: if self._config.auto_verify_all_known_devices:
if not Utils.verify_all_known_devices(self._client): if not Utils.verify_all_known_devices(self._client):
raise raise
return await self.send_content(room, content) return await self.content(room, content)
else: else:
raise raise
if type(result) is RoomSendResponse: if type(result) is RoomSendResponse:
@@ -134,11 +134,11 @@ class ClientSender:
else: else:
raise RuntimeError("Unknown error has occured", result) raise RuntimeError("Unknown error has occured", result)
async def send_text(self, async def text(self,
room: MatrixRoom | str, room: MatrixRoom | str,
text: str, text: str,
*, *,
is_html: bool | None = None) -> str: is_html: bool | None = None) -> str:
""" """
Send text message to `room`. Send text message to `room`.
@@ -156,15 +156,15 @@ class ClientSender:
"msgtype": "m.text", "msgtype": "m.text",
**text_data **text_data
} }
return (await self.send_content(room, content)).event_id return (await self.content(room, content)).event_id
async def send_image(self, async def image(self,
room: MatrixRoom | str, room: MatrixRoom | str,
path: Path | str, *, path: Path | str, *,
text: str | None = None, text: str | None = None,
is_html: bool | None = None, is_html: bool | None = None,
filename: str | None = None, filename: str | None = None,
timeout: float | None = 60 * 60) -> str: timeout: float | None = 60 * 60) -> str:
""" """
Send the image to `room`. Please note that formatted text is displayed Send the image to `room`. Please note that formatted text is displayed
incorrectly in some clients as of September 8th, 2026 incorrectly in some clients as of September 8th, 2026
@@ -213,16 +213,16 @@ class ClientSender:
"h": height "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, async def image_bytes(self,
room: MatrixRoom | str, room: MatrixRoom | str,
data: bytes, data: bytes,
filename: str, filename: str,
*, *,
text: str | None = None, text: str | None = None,
is_html: bool | None = None, is_html: bool | None = None,
timeout: float | None = 60 * 60) -> str: timeout: float | None = 60 * 60) -> str:
""" """
Send the image to `room`. Please note that formatted text is displayed Send the image to `room`. Please note that formatted text is displayed
incorrectly in some clients as of September 8th, 2026 incorrectly in some clients as of September 8th, 2026
@@ -276,16 +276,16 @@ class ClientSender:
"h": height "h": height
} }
} }
return (await self.send_content(room, content)).event_id return (await self.content(room, content)).event_id
async def send_video(self, async def video(self,
room: MatrixRoom | str, room: MatrixRoom | str,
path: Path | str, path: Path | str,
*, *,
props: VideoFileProperties | None = None, props: VideoFileProperties | None = None,
text: str | None = None, text: str | None = None,
is_html: bool | None = None, is_html: bool | None = None,
timeout: float | None = 60 * 60) -> str: timeout: float | None = 60 * 60) -> str:
""" """
Send the video to `room`. Please note that formatted text is displayed Send the video to `room`. Please note that formatted text is displayed
incorrectly in some clients as of September 8th, 2026. Unknown video incorrectly in some clients as of September 8th, 2026. Unknown video
@@ -361,17 +361,17 @@ class ClientSender:
} }
} }
# send # send
return (await self.send_content(room, content)).event_id return (await self.content(room, content)).event_id
async def send_file(self, async def file(self,
room: MatrixRoom | str, room: MatrixRoom | str,
path: Path | str, path: Path | str,
*, *,
filename: str | None = None, filename: str | None = None,
mime_type: str | None = None, mime_type: str | None = None,
text: str | None = None, text: str | None = None,
is_html: bool | None = None, is_html: bool | None = None,
timeout: float | None = 60 * 60): timeout: float | None = 60 * 60):
""" """
Send the file to `room`. Please note that formatted text is displayed Send the file to `room`. Please note that formatted text is displayed
incorrectly in some clients as of September 8th, 2026. incorrectly in some clients as of September 8th, 2026.
@@ -422,17 +422,17 @@ class ClientSender:
"size": upload_result.filesize "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, async def file_bytes(self,
room: MatrixRoom | str, room: MatrixRoom | str,
data: bytes, data: bytes,
filename: str, filename: str,
*, *,
mime_type: str | None = None, mime_type: str | None = None,
text: str | None = None, text: str | None = None,
is_html: bool | None = None, is_html: bool | None = None,
timeout: float | None = 60 * 60) -> str: timeout: float | None = 60 * 60) -> str:
""" """
Send the file to `room`. Send the file to `room`.
@@ -480,4 +480,4 @@ class ClientSender:
"size": upload_result.filesize "size": upload_result.filesize
} }
} }
return (await self.send_content(room, content)).event_id return (await self.content(room, content)).event_id

View File

@@ -12,6 +12,7 @@ from ..context import EventContext
from ._validation import Validator from ._validation import Validator
from ._storage import Storage from ._storage import Storage
from ._client_auth import ClientAuth from ._client_auth import ClientAuth
from ._client_room_operations import ClientRoomOperations
from ._client_downloader import ClientDownloader from ._client_downloader import ClientDownloader
from ._client_manager import ClientManager from ._client_manager import ClientManager
from ._client_uploader import ClientUploader from ._client_uploader import ClientUploader
@@ -35,6 +36,7 @@ class MatrixBot:
self._client_manager = ClientManager(self._client_auth, self._storage) self._client_manager = ClientManager(self._client_auth, self._storage)
self._client_uploader = ClientUploader(self._storage) self._client_uploader = ClientUploader(self._storage)
self._client_downloader = ClientDownloader() self._client_downloader = ClientDownloader()
self._client_room_operations = ClientRoomOperations()
self._client_sender = ClientSender() self._client_sender = ClientSender()
self._callbacks = Callbacks(self._storage, self) self._callbacks = Callbacks(self._storage, self)
# validate the config and save it # validate the config and save it
@@ -86,6 +88,10 @@ class MatrixBot:
self._config, self._config,
self._client_manager.get_client() self._client_manager.get_client()
) )
await self._client_room_operations.setup(
self._config,
self._client_manager.get_client()
)
await self._client_sender.setup( await self._client_sender.setup(
self._config, self._config,
self._client_manager.get_client(), self._client_manager.get_client(),
@@ -119,7 +125,8 @@ class MatrixBot:
finally: finally:
await self.stop() await self.stop()
def get_client(self) -> AsyncClient: @property
def client(self) -> AsyncClient:
""" """
Get AsyncClient. Get AsyncClient.
@@ -129,231 +136,19 @@ class MatrixBot:
""" """
return self._client_manager.get_client() return self._client_manager.get_client()
async def send_text(self, @property
room: MatrixRoom | str, def send(self) -> ClientSender:
text: str, """Get ClientSender that you should use to send messages."""
*, return self._client_sender
is_html: bool | None = None) -> str:
"""
Send text message to `room`.
Args: @property
- room - the room to send the text to def download(self) -> ClientDownloader:
- text - the text to send to the room """Get ClientDownloader that you should use to download files."""
- is_html - whether the text is HTML-formatted. Use `None` for auto return self._client_downloader
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, @property
room: MatrixRoom | str, def rooms(self) -> ClientRoomOperations:
path: Path | str, *, """Get ClientRoomOperations that you should use to do room-related
text: str | None = None, actions.
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 return self._client_room_operations
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
)

View File

@@ -123,7 +123,7 @@ class SenderIsBotFilter(BaseEventFilter):
async def __call__(self, context: EventContext) -> bool: async def __call__(self, context: EventContext) -> bool:
if not await super().__call__(context): if not await super().__call__(context):
return False return False
return context.bot.get_client().user_id == context.event.sender return context.bot.client.user_id == context.event.sender
class MessageHasFile(BaseEventFilter): class MessageHasFile(BaseEventFilter):
""" """