diff --git a/README.md b/README.md index 9f1e0fd10..fe2ebe45a 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ nanobot agent -m "Hello from my local LLM!" ## 💬 Chat Apps -Talk to your nanobot through Telegram, Discord, WhatsApp, or Feishu — anytime, anywhere. +Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu or Matrix(Element) — anytime, anywhere. | Channel | Setup | |---------|-------| diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index 1501a2888..06bc2a67a 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -95,7 +95,19 @@ class ChannelManager: logger.info("DingTalk channel enabled") except ImportError as e: logger.warning(f"DingTalk channel not available: {e}") - + + # Matrix channel + if self.config.channels.matrix.enabled: + try: + from nanobot.channels.matrix import MatrixChannel + self.channels["matrix"] = MatrixChannel( + self.config.channels.matrix, + self.bus + ) + logger.info("Matrix channel enabled") + except ImportError as e: + logger.warning(f"Matrix channel not available: {e}") + async def _start_channel(self, name: str, channel: BaseChannel) -> None: """Start a channel and log any exceptions.""" try: diff --git a/nanobot/channels/matrix.py b/nanobot/channels/matrix.py new file mode 100644 index 000000000..0ea4ae138 --- /dev/null +++ b/nanobot/channels/matrix.py @@ -0,0 +1,79 @@ +import asyncio +from typing import Any + +from nio import AsyncClient, MatrixRoom, RoomMessageText + +from nanobot.channels.base import BaseChannel +from nanobot.bus.events import OutboundMessage + + +class MatrixChannel(BaseChannel): + """ + Matrix (Element) channel using long-polling sync. + """ + + name = "matrix" + + def __init__(self, config: Any, bus): + super().__init__(config, bus) + self.client: AsyncClient | None = None + self._sync_task: asyncio.Task | None = None + + async def start(self) -> None: + self._running = True + + self.client = AsyncClient( + homeserver=self.config.homeserver, + user=self.config.user_id, + ) + + self.client.access_token = self.config.access_token + + self.client.add_event_callback( + self._on_message, + RoomMessageText + ) + + self._sync_task = asyncio.create_task(self._sync_loop()) + + async def stop(self) -> None: + self._running = False + if self._sync_task: + self._sync_task.cancel() + if self.client: + await self.client.close() + + async def send(self, msg: OutboundMessage) -> None: + if not self.client: + return + + await self.client.room_send( + room_id=msg.chat_id, + message_type="m.room.message", + content={"msgtype": "m.text", "body": msg.content}, + ) + + async def _sync_loop(self) -> None: + while self._running: + try: + await self.client.sync(timeout=30000) + except asyncio.CancelledError: + break + except Exception: + await asyncio.sleep(2) + + async def _on_message( + self, + room: MatrixRoom, + event: RoomMessageText + ) -> None: + # Ignore self messages + if event.sender == self.config.user_id: + return + + await self._handle_message( + sender_id=event.sender, + chat_id=room.room_id, + content=event.body, + metadata={"room": room.display_name}, + ) \ No newline at end of file diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index edea307a6..aa29403a2 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -46,6 +46,13 @@ class DiscordConfig(BaseModel): gateway_url: str = "wss://gateway.discord.gg/?v=10&encoding=json" intents: int = 37377 # GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT +class MatrixConfig(BaseModel): + """Matrix (Element) channel configuration.""" + enabled: bool = False + homeserver: str = "https://matrix.org" + access_token: str = "" + user_id: str = "" # @bot:matrix.org + allow_from: list[str] = Field(default_factory=list) class ChannelsConfig(BaseModel): """Configuration for chat channels.""" @@ -54,6 +61,7 @@ class ChannelsConfig(BaseModel): discord: DiscordConfig = Field(default_factory=DiscordConfig) feishu: FeishuConfig = Field(default_factory=FeishuConfig) dingtalk: DingTalkConfig = Field(default_factory=DingTalkConfig) + matrix: MatrixConfig = Field(default_factory=MatrixConfig) class AgentDefaults(BaseModel): diff --git a/pyproject.toml b/pyproject.toml index 6fda084c8..20afad6ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "python-telegram-bot[socks]>=21.0", "lark-oapi>=1.0.0", "socksio>=1.0.0", + "matrix-nio>=0.25.2" ] [project.optional-dependencies]