在 Discord 适配器中添加代理支持,包括配置模型、配置文件及实际代理实现。当配置了代理时,通过环境变量设置 HTTP/HTTPS 代理进行连接,以支持在需要代理的环境中使用 Discord 服务。
174 lines
7.2 KiB
Python
174 lines
7.2 KiB
Python
# -*- 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`")
|
||
|
||
# 必须声明 Intents,否则无法读取消息内容
|
||
intents = discord.Intents.default()
|
||
intents.message_content = True
|
||
|
||
# 检查是否配置了代理
|
||
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"
|
||
|
||
super().__init__(intents=intents)
|
||
self.token = token
|
||
self.logger = ModuleLogger("DiscordAdapter")
|
||
self.send_channel = None
|
||
|
||
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", [])
|
||
|
||
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}")
|
||
|
||
# 发送内容和附件(合并为一条消息)
|
||
if content or attachments:
|
||
await channel.send(content=content, files=[discord.File(fp=io.BytesIO(requests.get(attachment_url).content), filename=os.path.basename(attachment_url)) for attachment_url in attachments if attachment_url.startswith('http')] if attachments else None)
|
||
|
||
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...")
|
||
|
||
# 如果配置了代理,使用自定义的 ClientSession
|
||
if self.proxy:
|
||
import aiohttp
|
||
proxy_url = self.proxy
|
||
self.logger.info(f"[DiscordAdapter] 使用代理: {proxy_url} (类型: {self.proxy_type})")
|
||
|
||
connector = aiohttp.TCPConnector()
|
||
session = aiohttp.ClientSession(connector=connector)
|
||
|
||
# discord.py 2.0+ 使用 discord.Client 的 connector 参数
|
||
# 但 discord.Client 不直接支持自定义 connector
|
||
# 需要使用 discord.AutoShardedClient 或修改内部实现
|
||
# 这里我们使用 discord.Client 的 __init__ 传递 connector
|
||
# 但 discord.Client 的 __init__ 不支持 connector 参数
|
||
# 所以我们需要使用 discord.Client 的 _create_http_client 方法
|
||
|
||
# 简单方案:使用环境变量设置代理
|
||
import os
|
||
os.environ["HTTP_PROXY"] = proxy_url
|
||
os.environ["HTTPS_PROXY"] = proxy_url
|
||
|
||
self.logger.info("[DiscordAdapter] 代理已设置,正在连接 Discord...")
|
||
await self.start(self.token)
|
||
|
||
# 清理环境变量
|
||
os.environ.pop("HTTP_PROXY", None)
|
||
os.environ.pop("HTTPS_PROXY", None)
|
||
else:
|
||
await self.start(self.token)
|
||
except Exception as e:
|
||
self.logger.error(f"Discord 连接失败: {e}")
|