Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions matrix/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,8 @@ def build(self) -> dict:


@dataclass
class ImageContent(BaseMessageContent):
class ImageContent(FileContent):
msgtype = "m.image"
filename: str
url: str
mimetype: str
height: int = 0
width: int = 0

Expand All @@ -109,11 +106,8 @@ def build(self) -> dict:


@dataclass
class AudioContent(BaseMessageContent):
class AudioContent(FileContent):
msgtype = "m.audio"
filename: str
url: str
mimetype: str
duration: int = 0

def build(self) -> dict:
Expand All @@ -129,11 +123,8 @@ def build(self) -> dict:


@dataclass
class VideoContent(BaseMessageContent):
class VideoContent(FileContent):
msgtype = "m.video"
filename: str
url: str
mimetype: str
height: int = 0
width: int = 0
duration: int = 0
Expand Down
27 changes: 8 additions & 19 deletions matrix/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,36 +63,26 @@ async def reply(
raw: bool = False,
notice: bool = False,
file: File | None = None,
image: Image | None = None,
) -> Message:
"""Reply to the command with a message.

This is a convenience method that sends a message to the room where the
command was invoked. Supports text messages (with optional markdown
formatting) and file uploads (including images, videos, and audio).

See `Room.send()` for detailed usage examples and documentation.

## Example

```python
@bot.command()
async def hello(ctx: Context):
# Send a markdown-formatted reply
await ctx.reply("Hello **world**!")

@bot.command()
async def status(ctx: Context):
# Send a notice message
await ctx.reply("Bot is online!", notice=True)

@bot.command()
async def document(ctx: Context):
# Upload and send a file
with open("report.pdf", "rb") as f:
resp, _ = await ctx.room.client.upload(f, content_type="application/pdf")

file = File(
filename="report.pdf",
path=resp.content_uri,
mimetype="application/pdf"
)
await ctx.reply(file=file)

@bot.command()
async def cat(ctx: Context):
# Upload and send an image
Expand All @@ -105,13 +95,13 @@ async def cat(ctx: Context):
resp, _ = await ctx.room.client.upload(f, content_type="image/jpeg")

image = Image(
filename="cat.jpg",
path=resp.content_uri,
filename="cat.jpg",
mimetype="image/jpeg",
width=width,
height=height
)
await ctx.reply(image=image)
await ctx.reply(file=image)
```
"""

Expand All @@ -121,7 +111,6 @@ async def cat(ctx: Context):
raw=raw,
notice=notice,
file=file,
image=image,
)
except Exception as e:
raise MatrixError(f"Failed to send message: {e}")
Expand Down
195 changes: 137 additions & 58 deletions matrix/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
from matrix.errors import MatrixError
from matrix.message import Message
from matrix.content import (
BaseMessageContent,
TextContent,
MarkdownMessage,
NoticeContent,
ReplyContent,
FileContent,
ImageContent,
BaseMessageContent,
ReplyContent,
AudioContent,
VideoContent,
)
from matrix.types import File, Image
from matrix.types import File, Image, Audio, Video


class Room:
Expand Down Expand Up @@ -86,44 +88,37 @@ async def send(
raw: bool = False,
notice: bool = False,
file: File | None = None,
image: Image | None = None,
) -> Message:
"""Send a message to the room.

This is a convenience method that automatically routes to the appropriate
send method based on the provided arguments. Supports text messages (with
optional markdown formatting), file uploads, and image uploads.
optional markdown formatting) and file uploads (including images, videos, and audio).

For detailed text message examples, see `Room.send_text()`.
For detailed file upload examples, see `Room.send_file()`.

## Example

```python
# Send a markdown-formatted text message
await room.send("Hello **world**!")

# Send raw text without markdown
await room.send("Hello world!", raw=True)

# Send a notice message
await room.send("Bot is starting up...", notice=True)

# Send a file
file = File(filename="document.pdf", path="mxc://...", mimetype="application/pdf")
file = File(path="mxc://...", filename="document.pdf", mimetype="application/pdf")
await room.send(file=file)

# Send an image
image = Image(filename="photo.jpg", path="mxc://...", mimetype="image/jpeg", width=800, height=600)
await room.send(image=image)
image = Image(path="mxc://...", filename="photo.jpg", mimetype="image/jpeg", width=800, height=600)
await room.send(file=image)
```
"""
if content:
return await self.send_text(content, raw=raw, notice=notice)

if file:
return await self.send_file(file)

if image:
return await self.send_image(image)
raise ValueError("You must provide content, file, or image to send.")
raise ValueError("You must provide content or file.")

async def send_text(
self,
Expand All @@ -135,9 +130,9 @@ async def send_text(
) -> Message:
"""Send a text message to the room.

By default, messages are formatted using Markdown. You can send raw unformatted
text with `raw=True`, or send a notice message (typically used for bot status
updates) with `notice=True`.
By default, messages are formatted using Markdown. You can send raw unformatted
text with `raw=True`, or send a notice message (typically used for bot status
updates) with `notice=True`. Use `reply_to` to create a threaded reply.

## Example

Expand All @@ -152,7 +147,7 @@ async def send_text(
await room.send_text("Bot restarted successfully", notice=True)

# Reply to another message
await room.send_text("Bot restarted successfully", replay_to=message.id)
await room.send_text("Replying to you!", reply_to="$event_id")
```
"""
payload: TextContent
Expand All @@ -169,67 +164,151 @@ async def send_text(
return await self._send_payload(payload)

async def send_file(self, file: File) -> Message:
"""Send a file to the room.
"""Send a file, image, video, or audio to the room.

Accepts any File object or its subclasses (Image, Video, Audio). The file must
be uploaded to the Matrix content repository before sending. Use the room's
client upload method to get the MXC URI.

The file must be uploaded to the Matrix content repository before sending.
Use the room's client upload method to get the MXC URI for the file.
The method automatically detects the file type and sends it with the appropriate
Matrix message type (m.file, m.image, m.video, or m.audio).

For more information on the upload method, see the matrix-nio documentation:
https://matrix-nio.readthedocs.io/en/latest/nio.html#nio.AsyncClient.upload

## Example

```python
# Upload file first, then send
with open("document.pdf", "rb") as f:
resp, _ = await room.client.upload(f, content_type="application/pdf")
import os

# Send a document
file_path = "document.pdf"
file_size = os.path.getsize(file_path)

with open(file_path, "rb") as f:
resp, _ = await room.client.upload(
f,
content_type="application/pdf",
filesize=file_size
)

file = File(
filename="document.pdf",
path=resp.content_uri,
filename="document.pdf",
mimetype="application/pdf"
)
await room.send_file(file)
```
"""
payload = FileContent(
filename=file.filename, url=file.path, mimetype=file.mimetype
)
return await self._send_payload(payload)

async def send_image(self, image: Image) -> Message:
"""Send an image to the room.

The image must be uploaded to the Matrix content repository before sending.
Use the room's client upload method to get the MXC URI for the image.

## Example

```python
# Send an image
from PIL import Image as PILImage

# Get image dimensions
with PILImage.open("photo.jpg") as img:
image_path = "photo.jpg"

with PILImage.open(image_path) as img:
width, height = img.size

# Upload image first
with open("photo.jpg", "rb") as f:
resp, _ = await room.client.upload(f, content_type="image/jpeg")
file_size = os.path.getsize(image_path)

with open(image_path, "rb") as f:
resp, _ = await room.client.upload(
f,
content_type="image/jpeg",
filesize=file_size
)

image = Image(
filename="photo.jpg",
path=resp.content_uri,
filename="photo.jpg",
mimetype="image/jpeg",
width=width,
height=height
)
await room.send_image(image)
await room.send_file(image)

# Send a video
import cv2

video_path = "video.mp4"

cap = cv2.VideoCapture(video_path)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = int((frame_count / fps) * 1000)
cap.release()

file_size = os.path.getsize(video_path)

with open(video_path, "rb") as f:
resp, _ = await room.client.upload(
f,
content_type="video/mp4",
filesize=file_size
)

video = Video(
path=resp.content_uri,
filename="video.mp4",
mimetype="video/mp4",
width=width,
height=height,
duration=duration
)
await room.send_file(video)

# Send audio
audio_path = "audio.mp3"
file_size = os.path.getsize(audio_path)

with open(audio_path, "rb") as f:
resp, _ = await room.client.upload(
f,
content_type="audio/mpeg",
filesize=file_size
)

audio = Audio(
path=resp.content_uri,
filename="audio.mp3",
mimetype="audio/mpeg",
duration=180000 # 3 minutes in milliseconds
)
await room.send_file(audio)
```
"""
payload = ImageContent(
filename=image.filename,
url=image.path,
mimetype=image.mimetype,
height=image.height,
width=image.width,
)
payload: FileContent

match file:
case Image():
payload = ImageContent(
filename=file.filename,
url=file.path,
mimetype=file.mimetype,
height=file.height,
width=file.width,
)
case Audio():
payload = AudioContent(
filename=file.filename,
url=file.path,
mimetype=file.mimetype,
duration=file.duration,
)
case Video():
payload = VideoContent(
filename=file.filename,
url=file.path,
mimetype=file.mimetype,
height=file.height,
width=file.width,
duration=file.duration,
)
case _:
payload = FileContent(
filename=file.filename, url=file.path, mimetype=file.mimetype
)

return await self._send_payload(payload)

async def _send_payload(self, payload: BaseMessageContent) -> Message:
Expand Down
Loading