Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b22e7b4903 | |||
| 2867582c01 | |||
| 16b849ebd8 | |||
| 35bd68516a | |||
| 86257a70e3 | |||
| 43b5d8e3a6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ dist/
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
*.tmp
|
||||||
@@ -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,13 +23,13 @@ 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`
|
||||||
is needed for E2EE to work.
|
is needed for E2EE to work.
|
||||||
|
|
||||||
Please inspect [`examples/image_gen_bot.py`](examples/image_gen_bot.py),
|
Please inspect [`examples/image_bot.py`](examples/image_bot.py),
|
||||||
[`examples/echo_bot.py`](examples/echo_bot.py) or open [`examples/`](examples/)
|
[`examples/echo_bot.py`](examples/echo_bot.py) or open [`examples/`](examples/)
|
||||||
directory to find usage examples. Examples require that you set
|
directory to find usage examples. Examples require that you set
|
||||||
`MATRIX_HOMESERVER` and `MATRIX_USERNAME` environment variables. Examples create
|
`MATRIX_HOMESERVER` and `MATRIX_USERNAME` environment variables. Examples create
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
97
examples/file_bot.py
Normal file
97
examples/file_bot.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
This example implements Matrix bot that calculates SHA256 for a file sent by
|
||||||
|
user.
|
||||||
|
|
||||||
|
It uses environment variables to specify authorization data. Use Ctrl+C to stop
|
||||||
|
the bot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiofiles
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from mab import *
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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(
|
||||||
|
ctx.room,
|
||||||
|
"The file is larger than 1 MB, downloading to filesystem"
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sha.update(data)
|
||||||
|
does_temp_exist = True
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
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(
|
||||||
|
room=ctx.room,
|
||||||
|
data=sha.hexdigest().encode("utf-8"),
|
||||||
|
filename="hash of the file.txt",
|
||||||
|
mime_type="text/plain",
|
||||||
|
text=response
|
||||||
|
)
|
||||||
|
# resend the file to test uploading
|
||||||
|
if does_temp_exist:
|
||||||
|
await ctx.bot.send.file(
|
||||||
|
room=ctx.room,
|
||||||
|
path="temp.tmp",
|
||||||
|
filename=ctx[CTX_FILE_NAME],
|
||||||
|
text="This is the file you have sent, but it was reuploaded"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
os.unlink("temp.tmp")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
bot.add_callback(
|
||||||
|
~SenderIsBotFilter() & MessageHasFile() & NewMessageFilter(),
|
||||||
|
on_file_message)
|
||||||
|
|
||||||
|
# run until Ctrl+C
|
||||||
|
try:
|
||||||
|
await bot.run()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -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,17 +44,14 @@ 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
|
||||||
if not isinstance(data, bytes):
|
|
||||||
await ctx.bot.send_text(ctx.room, "😧")
|
|
||||||
return
|
|
||||||
# convert to Image
|
# convert to Image
|
||||||
with BytesIO(data) as buf:
|
with BytesIO(data) as buf:
|
||||||
img = Image.open(buf)
|
img = Image.open(buf)
|
||||||
@@ -69,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",
|
||||||
@@ -78,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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mab"
|
name = "mab"
|
||||||
version = "0.5.1"
|
version = "0.6.1"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Tyukalov Nikita", email = "nikita@tyukalov.su" }
|
{ name = "Tyukalov Nikita", email = "nikita@tyukalov.su" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ __all__ = [
|
|||||||
"CTX_CMD_PREFIX",
|
"CTX_CMD_PREFIX",
|
||||||
"CTX_CMD_VERB",
|
"CTX_CMD_VERB",
|
||||||
"CTX_CMD_ARGS",
|
"CTX_CMD_ARGS",
|
||||||
|
"CTX_FILE_SIZE",
|
||||||
|
"CTX_FILE_MIME",
|
||||||
|
"CTX_FILE_NAME",
|
||||||
|
|
||||||
# .bot
|
# .bot
|
||||||
"MatrixBot",
|
"MatrixBot",
|
||||||
@@ -50,4 +53,5 @@ __all__ = [
|
|||||||
"RedactedMessageFilter",
|
"RedactedMessageFilter",
|
||||||
"SenderIsFilter",
|
"SenderIsFilter",
|
||||||
"SenderIsBotFilter",
|
"SenderIsBotFilter",
|
||||||
|
"MessageHasFile",
|
||||||
]
|
]
|
||||||
@@ -2,10 +2,9 @@ import asyncio
|
|||||||
import aiofiles
|
import aiofiles
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
from typing import overload
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
from uuid import uuid4
|
||||||
import random
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -36,7 +35,7 @@ class ClientDownloader:
|
|||||||
config: MatrixBotConfig,
|
config: MatrixBotConfig,
|
||||||
client: AsyncClient) -> None:
|
client: AsyncClient) -> None:
|
||||||
"""
|
"""
|
||||||
Setup the sender.
|
Setup the downloader.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
- config - config to use
|
- config - config to use
|
||||||
@@ -151,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:
|
||||||
@@ -218,14 +229,12 @@ class ClientDownloader:
|
|||||||
if key is not None and iv is not None and sha256 is not None:
|
if key is not None and iv is not None and sha256 is not None:
|
||||||
# on disk
|
# on disk
|
||||||
if isinstance(result, Path):
|
if isinstance(result, Path):
|
||||||
r = unpaddedbase64.encode_base64(random.randbytes(8))
|
|
||||||
temp_path = result.with_name(
|
temp_path = result.with_name(
|
||||||
f".{result.name}.{int(time.time())}{r}.tmp"
|
f".{result.name}.{uuid4().hex}.tmp"
|
||||||
)
|
)
|
||||||
await self._decrypt_file(
|
await self._decrypt_file(
|
||||||
result, temp_path, key, iv, sha256
|
result, temp_path, key, iv, sha256
|
||||||
)
|
)
|
||||||
result.unlink()
|
|
||||||
temp_path.replace(result)
|
temp_path.replace(result)
|
||||||
temp_path = None
|
temp_path = None
|
||||||
# in memory
|
# in memory
|
||||||
|
|||||||
147
src/mab/bot/_client_room_operations.py
Normal file
147
src/mab/bot/_client_room_operations.py
Normal 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
|
||||||
@@ -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,4 +361,123 @@ class ClientSender:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
# send
|
# send
|
||||||
return (await self.send_content(room, content)).event_id
|
return (await self.content(room, content)).event_id
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 autodetect using content
|
||||||
|
of `path`)
|
||||||
|
- 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
|
||||||
|
"""
|
||||||
|
if self._client is None or self._config is None:
|
||||||
|
raise RuntimeError("ClientSender is not set up")
|
||||||
|
# get filename if not set
|
||||||
|
if not filename:
|
||||||
|
filename = os.path.basename(path)
|
||||||
|
# caption must not actually be empty
|
||||||
|
if text is None or not text.strip():
|
||||||
|
text = filename
|
||||||
|
is_html = False
|
||||||
|
# get the mime type if not specified
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = magic.from_file(path, mime=True)
|
||||||
|
# upload
|
||||||
|
async with asyncio.timeout(timeout):
|
||||||
|
upload_result = await self._uploader.upload_file(
|
||||||
|
path, mime_type=mime_type, filename=filename)
|
||||||
|
# send
|
||||||
|
content = {
|
||||||
|
"msgtype": "m.file",
|
||||||
|
"filename": filename,
|
||||||
|
**self._process_html_text(text, is_html),
|
||||||
|
"file": {
|
||||||
|
"url": upload_result.response.content_uri,
|
||||||
|
"mimetype": mime_type,
|
||||||
|
**upload_result.keys
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"mimetype": mime_type,
|
||||||
|
"size": upload_result.filesize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (await self.content(room, content)).event_id
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- room - the room to send the file to
|
||||||
|
- data - the content of the file to send
|
||||||
|
- filename - filename to use for the file
|
||||||
|
- mime_type - mime type to use (`None` for autodetect using `data`
|
||||||
|
content)
|
||||||
|
- 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
|
||||||
|
"""
|
||||||
|
# caption must not actually be empty
|
||||||
|
if text is None or not text.strip():
|
||||||
|
text = filename
|
||||||
|
is_html = False
|
||||||
|
# check if the file is image
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = magic.from_buffer(data, mime=True)
|
||||||
|
# upload
|
||||||
|
buffer = BytesIO(data)
|
||||||
|
async with asyncio.timeout(timeout):
|
||||||
|
upload_result = await self._uploader.upload_using_provider(
|
||||||
|
provider=buffer,
|
||||||
|
mime_type=mime_type,
|
||||||
|
filename=filename,
|
||||||
|
filesize=len(data))
|
||||||
|
# prepare the content and send
|
||||||
|
content = {
|
||||||
|
"msgtype": "m.file",
|
||||||
|
"filename": filename,
|
||||||
|
**self._process_html_text(text, is_html),
|
||||||
|
"file": {
|
||||||
|
"url": upload_result.response.content_uri,
|
||||||
|
"mimetype": mime_type,
|
||||||
|
**upload_result.keys
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"mimetype": mime_type,
|
||||||
|
"size": upload_result.filesize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (await self.content(room, content)).event_id
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import Callable, Coroutine, Any
|
from typing import Callable, Coroutine, Any, overload
|
||||||
|
|
||||||
from nio import AsyncClient, MatrixRoom, Event
|
from nio import AsyncClient, MatrixRoom, Event
|
||||||
|
|
||||||
@@ -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,148 +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:
|
|
||||||
|
@property
|
||||||
|
def download(self) -> ClientDownloader:
|
||||||
|
"""Get ClientDownloader that you should use to download files."""
|
||||||
|
return self._client_downloader
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rooms(self) -> ClientRoomOperations:
|
||||||
|
"""Get ClientRoomOperations that you should use to do room-related
|
||||||
|
actions.
|
||||||
"""
|
"""
|
||||||
Send text message to `room`.
|
return self._client_room_operations
|
||||||
|
|
||||||
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 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
|
|
||||||
)
|
|
||||||
@@ -34,6 +34,15 @@ CTX_CMD_ARGS = ContextDataKey[list[str]]("CTX_CMD_ARGS")
|
|||||||
CTX_ROOM_ENCRYPTED = ContextDataKey[bool]("CTX_ROOM_ENCRYPTED")
|
CTX_ROOM_ENCRYPTED = ContextDataKey[bool]("CTX_ROOM_ENCRYPTED")
|
||||||
"""True if the room is encrypted"""
|
"""True if the room is encrypted"""
|
||||||
|
|
||||||
|
CTX_FILE_SIZE = ContextDataKey[int]("CTX_FILE_SIZE")
|
||||||
|
"""Size of the file attached to the message (bytes)"""
|
||||||
|
|
||||||
|
CTX_FILE_MIME = ContextDataKey[str]("CTX_FILE_MIME")
|
||||||
|
"""Mime type of the file attached to the message"""
|
||||||
|
|
||||||
|
CTX_FILE_NAME = ContextDataKey[str]("CTX_FILE_NAME")
|
||||||
|
"""Name of the file attached to the message"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# EventContext implementation
|
# EventContext implementation
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import traceback
|
import traceback
|
||||||
from .base import BaseEventFilter, EventTypeFilter
|
from .base import BaseEventFilter, EventTypeFilter
|
||||||
from ..types import MessageType
|
from ..types import MessageType
|
||||||
from ..context import EventContext, CTX_MESSAGE_TYPE, CTX_SENDER
|
from ..context import (EventContext,
|
||||||
|
CTX_MESSAGE_TYPE,
|
||||||
|
CTX_SENDER,
|
||||||
|
CTX_FILE_SIZE,
|
||||||
|
CTX_FILE_MIME,
|
||||||
|
CTX_FILE_NAME)
|
||||||
|
|
||||||
from nio import AsyncClient
|
from nio import AsyncClient
|
||||||
from nio import MatrixRoom, Event
|
from nio import MatrixRoom, Event
|
||||||
@@ -118,4 +123,29 @@ 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):
|
||||||
|
"""
|
||||||
|
This filter returns True if the message contains file that can be
|
||||||
|
downloaded.
|
||||||
|
|
||||||
|
This filter sets `CTX_FILE_NAME`, `CTX_FILE_SIZE` and `CTX_FILE_MIME`
|
||||||
|
variables in the context (if they are present in the).
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
async def __call__(self, context: EventContext) -> bool:
|
||||||
|
if not await super().__call__(context):
|
||||||
|
return False
|
||||||
|
content: dict | None = context.event.source.get("content")
|
||||||
|
if content is None:
|
||||||
|
return False
|
||||||
|
# `file` if encrypted, `url` if not encrypted
|
||||||
|
if "file" not in content and "url" not in content:
|
||||||
|
return False
|
||||||
|
context[CTX_FILE_NAME] = content.get("filename") or content.get("body")
|
||||||
|
context[CTX_FILE_SIZE] = content["info"].get("size")
|
||||||
|
context[CTX_FILE_MIME] = content["info"].get("mimetype")
|
||||||
|
return True
|
||||||
Reference in New Issue
Block a user