"""Discord channel — bot integration via discord.py.""" from __future__ import annotations import asyncio import os import discord from loguru import logger from xtrm_agent.bus import InboundMessage, MessageBus, OutboundMessage from xtrm_agent.channels.base import BaseChannel class DiscordChannel(BaseChannel): """Discord bot channel.""" def __init__( self, bus: MessageBus, token_env: str = "DISCORD_BOT_TOKEN", default_agent: str = "coder", allowed_users: list[str] | None = None, ) -> None: super().__init__(bus) self.token_env = token_env self.default_agent = default_agent self.allowed_users = set(allowed_users or []) self._outbound_queue = bus.subscribe_outbound("discord") intents = discord.Intents.default() intents.message_content = True self.client = discord.Client(intents=intents) self._setup_events() def _setup_events(self) -> None: @self.client.event async def on_ready() -> None: logger.info(f"Discord bot connected as {self.client.user}") @self.client.event async def on_message(message: discord.Message) -> None: if message.author == self.client.user: return if message.author.bot: return # Check allowlist if self.allowed_users and str(message.author.id) not in self.allowed_users: return # Only respond to mentions or DMs is_dm = isinstance(message.channel, discord.DMChannel) is_mentioned = self.client.user in message.mentions if self.client.user else False if not is_dm and not is_mentioned: return content = message.content # Strip bot mention from content if self.client.user: content = content.replace(f"<@{self.client.user.id}>", "").strip() msg = InboundMessage( channel="discord", sender_id=str(message.author.id), chat_id=str(message.channel.id), content=content, metadata={"guild_id": str(message.guild.id) if message.guild else ""}, ) await self.bus.publish_inbound(msg) # Wait for response and send it try: async with message.channel.typing(): out = await asyncio.wait_for(self._outbound_queue.get(), timeout=300) await self._send_chunked(message.channel, out.content) except asyncio.TimeoutError: await message.channel.send("Sorry, I timed out processing your request.") async def _send_chunked( self, channel: discord.abc.Messageable, content: str ) -> None: """Send a message, splitting into 2000-char chunks if needed.""" while content: chunk = content[:2000] content = content[2000:] await channel.send(chunk) async def start(self) -> None: token = os.environ.get(self.token_env) if not token: logger.error(f"Discord token not found in env var '{self.token_env}'") return logger.info("Starting Discord bot...") await self.client.start(token) async def stop(self) -> None: await self.client.close()