from __future__ import annotations from pathlib import Path from typing import Any from loguru import logger from opc.channels.provider_base import SocketChannel from opc.core.models import SystemMessage class DiscordChannel(SocketChannel): name = "discord" required_package = "discord" def __init__(self, config: Any, bus: Any): super().__init__(config, bus) self._client: Any = None self._bot_user_id: str | None = None def get_required_config_fields(self) -> list[str]: return ["token"] def normalize_event(self, payload: dict[str, Any]) -> dict[str, Any]: author = payload.get("author", {}) channel_id = str(payload.get("channel_id", "") or "") thread_id = "" if payload.get("is_thread"): thread_id = str(payload.get("thread_id", channel_id) or "") attachments = [] for item in list(payload.get("attachments", []) or []): attachments.append( { "url": str(item.get("url", "") or ""), "filename": str(item.get("filename", "") or ""), "content_type": str(item.get("content_type", "") or ""), } ) return { "sender_id": str(author.get("id", "")), "chat_id": channel_id, "content": str(payload.get("content", "") or ""), "thread_id": thread_id, "reply_to": str((payload.get("message_reference") or {}).get("message_id", "") or ""), "attachments": attachments, "metadata": { "channel_type": str(payload.get("channel_type", "") or ""), "guild_id": str(payload.get("guild_id", "") or ""), "message_id": str(payload.get("id", "") or ""), "mentions_bot": bool(payload.get("mentions_bot", False)), }, } def should_accept_inbound(self, normalized: dict[str, Any]) -> bool: sender_id = str(normalized.get("sender_id", "") or "") if not sender_id or sender_id == self._bot_user_id: return False metadata = dict(normalized.get("metadata", {}) or {}) channel_type = str(metadata.get("channel_type", "") or "") if channel_type == "dm": return self.is_allowed(sender_id) if self.config.group_policy == "open": return self.is_allowed(sender_id) if self.config.group_policy == "allowlist": return str(normalized.get("chat_id", "") or "") in list(getattr(self.config, "group_allow_from", []) or []) return bool(metadata.get("mentions_bot")) async def run_socket_forever(self) -> None: import discord self._client = self._build_client(discord) await self._client.start(self.config.token) def _build_client(self, discord_module: Any) -> Any: intents = discord_module.Intents.default() intents.message_content = True intents.guild_messages = True intents.dm_messages = True intents.guilds = True channel = self class OPCDiscordClient(discord_module.Client): async def on_ready(self) -> None: channel._bot_user_id = str(self.user.id) if self.user else None logger.info("discord bot connected as {}", self.user) async def on_message(self, message: Any) -> None: if not message or not getattr(message, "author", None): return payload = { "id": str(message.id), "author": {"id": str(message.author.id)}, "channel_id": str(message.channel.id), "guild_id": str(getattr(message.guild, "id", "") or ""), "content": str(message.content or ""), "is_thread": bool(getattr(message.channel, "thread", None) or getattr(message.channel, "parent", None)), "thread_id": str(getattr(message.channel, "id", "") if isinstance(message.channel, discord_module.Thread) else ""), "channel_type": "dm" if isinstance(message.channel, discord_module.DMChannel) else "guild", "mentions_bot": bool(self.user and self.user in getattr(message, "mentions", [])), "attachments": [ { "url": str(att.url), "filename": str(att.filename), "content_type": str(att.content_type or ""), } for att in list(getattr(message, "attachments", []) or []) ], } if getattr(message, "reference", None) and getattr(message.reference, "message_id", None): payload["message_reference"] = {"message_id": str(message.reference.message_id)} await channel.publish_normalized(channel.normalize_event(payload)) return OPCDiscordClient(intents=intents) async def stop(self) -> None: if self._client is not None: try: await self._client.close() except Exception: logger.exception("discord client close failed") self._client = None await super().stop() async def send(self, message: SystemMessage) -> None: await super().send(message) if self._client is None: logger.warning("discord client not connected") return metadata = dict(message.metadata or {}) channel_id = int(str(metadata.get("chat_id") or message.session_id)) channel = self._client.get_channel(channel_id) or await self._client.fetch_channel(channel_id) reference = None reply_to = str(metadata.get("reply_to", "") or "") if reply_to: try: reference = await channel.fetch_message(int(reply_to)) except Exception: reference = None files = [] for attachment in list(metadata.get("attachments", []) or []): if isinstance(attachment, str) and Path(attachment).is_file(): import discord files.append(discord.File(attachment)) if files: await channel.send(content=message.content or None, files=files, reference=reference) elif message.content.strip(): await channel.send(content=message.content, reference=reference)