Files
NeoBot/adapters/discord_adapter.py
K2Cr2O1 3814f49fcf feat(跨平台): 增强跨平台消息互通功能
- 支持合并转发消息解析和展示
- 优化附件处理逻辑,支持文件名和类型识别
- 添加 Discord Embed 卡片支持,提升消息展示效果
- 重构消息格式化和转发逻辑,提高可维护性
- 更新代理配置和日志级别设置
2026-03-15 16:48:26 +08:00

183 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Discord 适配器 (Discord Adapter)
此模块负责与 Discord API 建立连接,接收 Discord 消息,
并将其转换为通用数据模型 (Universal Data Models)
同时提供将通用消息段发送回 Discord 的能力。
"""
import asyncio
import json
import os
import io
import requests
from typing import Union, List, Optional
try:
import discord
DISCORD_AVAILABLE = True
except ImportError:
DISCORD_AVAILABLE = False
from core.utils.logger import ModuleLogger
from .router import DiscordToOneBotConverter
from core.managers.redis_manager import redis_manager
from core.config_loader import global_config
class DiscordAdapter(discord.Client if DISCORD_AVAILABLE else object):
"""
Discord 客户端适配器。
继承自 discord.Client负责处理 Discord 的底层事件。
"""
def __init__(self, token: str):
if not DISCORD_AVAILABLE:
raise ImportError("discord.py 未安装,请运行 `pip install discord.py`")
self.logger = ModuleLogger("DiscordAdapter")
self.token = token
self.send_channel = None
self.proxy = None
self.proxy_type = "http"
if global_config.discord.proxy:
self.proxy = global_config.discord.proxy
self.proxy_type = global_config.discord.proxy_type or "http"
proxy_url = self.proxy
if self.proxy_type.lower() in ["socks5", "socks4"]:
if not proxy_url.startswith(("socks5://", "socks4://")):
proxy_url = f"{self.proxy_type.lower()}://{proxy_url.split('://')[-1]}"
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
self.logger.info(f"[DiscordAdapter] 代理已设置: {proxy_url} (类型: {self.proxy_type})")
intents = discord.Intents.default()
intents.message_content = True
super().__init__(intents=intents)
async def on_ready(self):
"""当 Bot 成功连接到 Discord 时触发"""
self.logger.success(f"Discord Bot 已登录: {self.user} (ID: {self.user.id})")
# 启动 Redis 订阅以处理跨平台消息
asyncio.create_task(self.start_redis_subscription())
async def on_message(self, message: 'discord.Message'):
"""当收到 Discord 消息时触发"""
# 忽略机器人自己的消息
if message.author.bot:
return
self.logger.info(f"[Discord 消息] {message.author}: {message.content}")
# 1. 将 discord.Message 伪装成 OneBot 事件模型
# 2. 触发业务逻辑
# 将伪装后的事件丢给现有的命令管理器 (matcher)
from core.managers.command_manager import matcher
# matcher.handle_event 需要 bot 实例和 event 实例
# 我们在 create_mock_event 中已经注入了一个假的 bot 对象
try:
mock_event = DiscordToOneBotConverter.create_mock_event(message, self)
await matcher.handle_event(mock_event.bot, mock_event)
except Exception as e:
self.logger.error(f"处理 Discord 消息时发生异常: {e}")
async def start_redis_subscription(self):
"""启动 Redis 订阅以处理跨平台消息发送"""
if redis_manager.redis is None:
self.logger.warning("[DiscordAdapter] Redis 未初始化,跳过订阅")
return
try:
channel_name = "neobot_discord_send"
pubsub = redis_manager.redis.pubsub()
await pubsub.subscribe(channel_name)
self.logger.success(f"[DiscordAdapter] 已订阅 Redis 频道: {channel_name}")
async for message in pubsub.listen():
if message["type"] == "message":
try:
data = json.loads(message["data"])
if data.get("type") == "send_message":
await self.handle_send_message(data)
except json.JSONDecodeError as e:
self.logger.error(f"[DiscordAdapter] 解析 Redis 消息失败: {e}")
except Exception as e:
self.logger.error(f"[DiscordAdapter] 处理 Redis 消息失败: {e}")
except Exception as e:
self.logger.error(f"[DiscordAdapter] Redis 订阅异常: {e}")
async def handle_send_message(self, data: dict):
"""处理来自 Redis 的消息发送请求"""
try:
channel_id = data.get("channel_id")
content = data.get("content", "")
attachments = data.get("attachments", [])
embed_data = data.get("embed")
if channel_id is None:
self.logger.error("[DiscordAdapter] 缺少 channel_id")
return
channel = self.get_channel(channel_id)
if channel is None:
self.logger.error(f"[DiscordAdapter] 未找到频道: {channel_id}")
return
self.logger.info(f"[DiscordAdapter] 正在发送消息到频道 {channel_id}")
files = []
if attachments:
proxies = None
if self.proxy:
proxies = {
"http": self.proxy,
"https": self.proxy
}
for attachment in attachments:
if isinstance(attachment, dict):
attachment_url = attachment.get("url", "")
filename = attachment.get("filename", "")
else:
attachment_url = str(attachment)
filename = ""
if attachment_url.startswith('http'):
try:
response = requests.get(attachment_url, proxies=proxies, timeout=30)
if not filename:
filename = os.path.basename(attachment_url.split('?')[0]) or "attachment"
files.append(discord.File(fp=io.BytesIO(response.content), filename=filename))
except Exception as e:
self.logger.error(f"[DiscordAdapter] 下载附件失败: {attachment_url}, 错误: {e}")
embed = None
if embed_data:
embed = discord.Embed.from_dict(embed_data)
if content or files or embed:
await channel.send(content=content, files=files if files else None, embed=embed)
self.logger.success(f"[DiscordAdapter] 消息已发送到频道 {channel_id}")
except Exception as e:
self.logger.error(f"[DiscordAdapter] 发送消息失败: {e}")
async def start_client(self):
"""启动 Discord 客户端(非阻塞方式)"""
if not DISCORD_AVAILABLE:
self.logger.error("无法启动 Discord 客户端discord.py 未安装")
return
try:
self.logger.info("正在连接 Discord...")
await self.start(self.token)
except Exception as e:
self.logger.error(f"Discord 连接失败: {e}")