Skip to content
Open
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
20 changes: 20 additions & 0 deletions examples/table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from matrix import Bot, Table

bot = Bot()


@bot.command()
async def weather(ctx):
weather = Table(title="Los Angeles", columns=2)

weather.add_field("Description", "Clear Sky")
weather.add_field("Visibility", "10000m | 32808ft")
weather.add_field("Temperature", "71.33°F | 21.85°C")
weather.add_field("Feels Like", "71.33°F | 21.85°C")
weather.add_field("Atmospheric Pressure", "1012 hPa")
weather.add_field("Humidity", "66%")

await ctx.reply(component=weather)


bot.start(config="config.yaml")
2 changes: 2 additions & 0 deletions matrix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .space import Space
from .message import Message
from .extension import Extension
from .component import Table

__all__ = [
"Bot",
Expand All @@ -32,4 +33,5 @@
"Space",
"Message",
"Extension",
"Table",
]
59 changes: 59 additions & 0 deletions matrix/component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from html import escape
from abc import ABC, abstractmethod


class Component(ABC):
"""Base class for message components."""

@abstractmethod
def to_plain_text(self) -> str:
pass

@abstractmethod
def render(self) -> str:
pass


class Table(Component):
def __init__(self, *, title: str, columns: int = 2) -> None:
self.title: str = title
self.columns: int = columns
self.fields: list[tuple[str, str]] = []

def __str__(self) -> str:
return self.render()

def add_field(self, name: str, value: str) -> None:
self.fields.append((name, value))

def to_plain_text(self) -> str:
return "\n".join(
[self.title, *[f"{name}: {value}" for name, value in self.fields]]
)

def render(self) -> str:
cells = [f"""
<td>
<strong>{escape(name)}</strong><br>
{escape(value)}
</td>
""" for name, value in self.fields]

rows = ""
for i in range(0, len(cells), self.columns):
row_cells = cells[i : i + self.columns]

while len(row_cells) < self.columns:
row_cells.append("<td></td>")

rows += f"<tr>{''.join(row_cells)}</tr>"

return f"""<blockquote>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the blockquote?

<h2>{escape(self.title)}</h2>
<table>
<tbody>
{rows}
</tbody>
</table>
</blockquote>
""".strip()
15 changes: 15 additions & 0 deletions matrix/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from dataclasses import dataclass
from markdown import markdown
from typing import Any
from .component import Component


class BaseMessageContent(ABC):
Expand Down Expand Up @@ -172,3 +173,17 @@ def build(self) -> dict:
"key": self.emoji,
}
}


@dataclass
class ComponentContent(BaseMessageContent):
msgtype = "m.text"
component: Component

def build(self) -> dict:
return {
"msgtype": self.msgtype,
"body": self.component.to_plain_text(),
"format": "org.matrix.custom.html",
"formatted_body": self.component.render(),
}
5 changes: 4 additions & 1 deletion matrix/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from .errors import MatrixError
from .message import Message
from .room import Room
from .types import File, Image
from .types import File
from matrix.component import Component

if TYPE_CHECKING:
from .bot import Bot # pragma: no cover
Expand Down Expand Up @@ -56,6 +57,7 @@ async def reply(
self,
content: str | None = None,
*,
component: Component | None = None,
raw: bool = False,
notice: bool = False,
file: File | None = None,
Expand Down Expand Up @@ -104,6 +106,7 @@ async def cat(ctx: Context):
try:
return await self.room.send(
content,
component=component,
raw=raw,
notice=notice,
file=file,
Expand Down
28 changes: 28 additions & 0 deletions matrix/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from nio import AsyncClient, MatrixRoom, Event

from matrix.component import Component
from matrix.api import matrix_call
from matrix.message import Message
from matrix.content import (
Expand All @@ -14,6 +15,7 @@
ImageContent,
AudioContent,
VideoContent,
ComponentContent,
)
from matrix.types import File, Image, Audio, Video

Expand Down Expand Up @@ -101,6 +103,7 @@ async def send(
self,
content: str | None = None,
*,
component: Component | None = None,

@PenguinBoi12 PenguinBoi12 Jul 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer this to be after file in the list of parameters/keep raw and notice the 2 first after the *

Same for context.

raw: bool = False,
notice: bool = False,
file: File | None = None,
Expand All @@ -117,6 +120,10 @@ async def send(
## Example

```python
# Send component-formatted message
table = Table(title="Los Angeles")
await room.send(component=table)

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

Expand All @@ -129,13 +136,34 @@ async def send(
await room.send(file=image)
```
"""
if component:
return await self.send_component(component)

if content:
return await self.send_text(content, raw=raw, notice=notice)

if file:
return await self.send_file(file)
raise ValueError("You must provide content or file.")

async def send_component(
self,
component: Component,
) -> Message:
"""Send a component-formatted message to the room.

## Example

```python
# Send component-formatted message
table = Table(title="Los Angeles")
await room.send_component(table)
```
"""
payload: ComponentContent = ComponentContent(component=component)

return await self._send_payload(payload)

async def send_text(
self,
content: str,
Expand Down
Loading