Files
OpenOPC/opc/channels/discord.py
T
2026-07-01 17:56:31 +08:00

149 lines
6.4 KiB
Python

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)