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
36 changes: 18 additions & 18 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[dev]
- name: Lint with Black
run: |
black --check matrix/ tests/ examples/
- name: Check typing with mypy
run: |
mypy matrix
- name: Test with pytest
run: |
pytest -v
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v3
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .[dev]
- name: Lint with Black
run: |
black --check matrix/ tests/ examples/
- name: Check typing with mypy
run: |
mypy matrix
- name: Test with pytest
run: |
pytest -v
37 changes: 12 additions & 25 deletions examples/reaction.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,38 @@
from asyncio import Event
from nio import MatrixRoom, RoomMessageText, ReactionEvent
from matrix import Bot
from matrix.message import Message
from matrix import Bot, Room, Message

bot = Bot()


@bot.event
async def on_message(room: MatrixRoom, event: RoomMessageText) -> None:
async def on_message(room: Room, event: RoomMessageText) -> None:
"""
This function listens for new messages in a room and reacts based
on the message content.
"""
room = bot.get_room(room.room_id)
message = Message(
room=room,
event_id=event.event_id,
body=event.body,
client=bot.client,
)

if event.body.lower().startswith("thanks"):
message = await room.fetch_message(event.event_id)

if message.body.lower().startswith("thanks"):
await message.react("🙏")

if event.body.lower().startswith("hello"):
if message.body.lower().startswith("hello"):
# Can also react with a text message instead of emoji
await message.react("hi")

if event.body.lower().startswith("❤️"):
if message.body.lower().startswith("❤️"):
await message.react("❤️")


@bot.event
async def on_react(room: MatrixRoom, event: ReactionEvent) -> None:
async def on_react(room: Room, event: ReactionEvent) -> None:
"""
This function listens for new member reaction to messages in a room,
and reacts based on the reaction emoji.
"""
room = bot.get_room(room.room_id)
message = Message(
room=room,
event_id=event.event_id,
body=None,
client=bot.client,
)
emoji = event.key

if emoji == "🙏":
message = await room.fetch_message(event.reacts_to)

if message.key == "🙏":
await message.react("❤️")


Expand Down
2 changes: 2 additions & 0 deletions matrix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .help import HelpCommand
from .checks import cooldown
from .room import Room
from .message import Message
from .extension import Extension

__all__ = [
Expand All @@ -27,5 +28,6 @@
"HelpCommand",
"cooldown",
"Room",
"Message",
"Extension",
]
11 changes: 6 additions & 5 deletions matrix/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,10 @@ async def run(self) -> None:

# MATRIX EVENTS

async def on_message(self, room: MatrixRoom, event: Event) -> None:
async def on_message(self, room: Room, event: Event) -> None:
await self._process_commands(room, event)

async def _on_matrix_event(self, room: MatrixRoom, event: Event) -> None:
async def _on_matrix_event(self, matrix_room: MatrixRoom, event: Event) -> None:
# ignore bot events
if event.sender == self.client.user:
return
Expand All @@ -275,6 +275,7 @@ async def _on_matrix_event(self, room: MatrixRoom, event: Event) -> None:
return

try:
room = self.get_room(matrix_room.room_id)
await self._dispatch_matrix_event(room, event)
except Exception as error:
await self._on_error(error)
Expand All @@ -284,14 +285,14 @@ async def _dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None:
for handler in self._hook_handlers.get(event_name, []):
await handler(*args, **kwargs)

async def _dispatch_matrix_event(self, room: MatrixRoom, event: Event) -> None:
async def _dispatch_matrix_event(self, room: Room, event: Event) -> None:
"""Fire all listeners registered for a named matrix event."""
for event_type, funcs in self._event_handlers.items():
if isinstance(event, event_type):
for func in funcs:
await func(room, event)

async def _process_commands(self, room: MatrixRoom, event: Event) -> None:
async def _process_commands(self, room: Room, event: Event) -> None:
"""Parse and execute commands"""
ctx = await self._build_context(room, event)

Expand All @@ -303,7 +304,7 @@ async def _process_commands(self, room: MatrixRoom, event: Event) -> None:
await self._on_command(ctx)
await ctx.command(ctx)

async def _build_context(self, matrix_room: MatrixRoom, event: Event) -> Context:
async def _build_context(self, matrix_room: Room, event: Event) -> Context:
room = self.get_room(matrix_room.room_id)
ctx = Context(bot=self, room=room, event=event)
prefix = self.prefix or self.config.prefix
Expand Down
83 changes: 61 additions & 22 deletions matrix/message.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import TYPE_CHECKING
from nio import AsyncClient
from typing import TYPE_CHECKING, Self

from nio import AsyncClient, Event

from matrix.types import Reaction
from matrix.content import ReactionContent, EditContent
from matrix.errors import MatrixError

Expand All @@ -10,38 +13,77 @@
class Message:
"""Represents a Matrix message with methods to interact with it."""

def __init__(
self, *, room: "Room", event_id: str, body: str | None, client: AsyncClient
) -> None:
def __init__(self, *, room: "Room", event: Event, client: AsyncClient) -> None:
self._room = room
self._event_id = event_id
self._body = body
self._matrix_event: Event = event
self._client = client

self._body = getattr(self._matrix_event, "body", None)

def __repr__(self) -> str:
return f"<Message id={self.event_id!r} body={self.body!r}>"

@property
def room(self) -> "Room":
"""The room this message was sent in."""
return self._room

@property
def id(self) -> str:
"""The event ID of this message."""
return self._event_id
def event(self) -> Event:
"""The matrix event of this message"""
return self._matrix_event

@property
def client(self) -> AsyncClient:
"""The Matrix client."""
return self._client

@property
def event_id(self) -> str:
"""The event ID of this message (alias for id)."""
return self._event_id
"""The event ID of this message."""
return str(self._matrix_event.event_id)

@property
def body(self) -> str | None:
"""The text content of this message."""
return self._body

@property
def client(self) -> AsyncClient:
"""The Matrix client."""
return self._client
def key(self) -> str | None:
"""The key of this message."""
return getattr(self._matrix_event, "key", None)

async def fetch_reactions(self) -> list[Reaction]:
"""Fetch all reactions for this message.

Returns a dict mapping emoji to a list of sender IDs who reacted with it.

## Example
```python
@bot.command()
async def reactions(ctx: Context):
reactions = await ctx.message.fetch_reactions()

for emoji, senders in reactions.items():
await ctx.reply(f"{emoji}: {len(senders)} reaction(s)")
```
"""
raw: dict[str, list[str]] = {}

try:
async for event in self.client.room_get_event_relations(
room_id=self.room.room_id,
event_id=self.event_id,
):
emoji = getattr(event, "key", None)
sender = getattr(event, "sender", None)

if emoji and sender:
raw.setdefault(emoji, []).append(sender)
except Exception as e:
raise MatrixError(f"Failed to fetch reactions: {e}")

return [Reaction(key=emoji, senders=senders) for emoji, senders in raw.items()]

async def reply(self, body: str) -> "Message":
"""Reply to this message.
Expand All @@ -57,7 +99,7 @@ async def echo(ctx: Context):
```
"""
try:
return await self.room.send_text(content=body, reply_to=self.id)
return await self.room.send_text(content=body, reply_to=self.event_id)
except Exception as e:
raise MatrixError(f"Failed to send reply: {e}")

Expand All @@ -72,7 +114,7 @@ async def thumbsup(ctx: Context):
await msg.react("👍")
```
"""
content = ReactionContent(event_id=self.id, emoji=emoji)
content = ReactionContent(event_id=self.event_id, emoji=emoji)

try:
await self.client.room_send(
Expand All @@ -95,7 +137,7 @@ async def typo(ctx: Context):
await msg.edit("Hello world!")
```
"""
content = EditContent(new_body, original_event_id=self.id)
content = EditContent(new_body, original_event_id=self.event_id)

try:
await self.client.room_send(
Expand All @@ -122,10 +164,7 @@ async def oops(ctx: Context):
try:
await self.client.room_redact(
room_id=self.room.room_id,
event_id=self.id,
event_id=self.event_id,
)
except Exception as e:
raise MatrixError(f"Failed to delete message: {e}")

def __repr__(self) -> str:
return f"<Message id={self.id!r} body={self.body!r}>"
Loading
Loading