Merge pull request #5464 from ChanningHe/enhance/discord

This commit is contained in:
jxxghp
2026-02-04 18:08:38 +08:00
committed by GitHub
4 changed files with 150 additions and 19 deletions

View File

@@ -139,9 +139,23 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
发送通知消息
:param message: 消息通知对象
"""
for conf in self.get_configs().values():
# DEBUG: Log entry and configs
configs = self.get_configs()
logger.debug(f"[Discord] post_message 被调用message.source={message.source}, "
f"message.userid={message.userid}, message.channel={message.channel}")
logger.debug(f"[Discord] 当前配置数量: {len(configs)}, 配置名称: {list(configs.keys())}")
logger.debug(f"[Discord] 当前实例数量: {len(self.get_instances())}, 实例名称: {list(self.get_instances().keys())}")
if not configs:
logger.warning("[Discord] get_configs() 返回空,没有可用的 Discord 配置")
return
for conf in configs.values():
logger.debug(f"[Discord] 检查配置: name={conf.name}, type={conf.type}, enabled={conf.enabled}")
if not self.check_message(message, conf.name):
logger.debug(f"[Discord] check_message 返回 False跳过配置: {conf.name}")
continue
logger.debug(f"[Discord] check_message 通过,准备发送到: {conf.name}")
targets = message.targets
userid = message.userid
if not userid and targets is not None:
@@ -150,13 +164,18 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
logger.warn("用户没有指定 Discord 用户ID消息无法发送")
return
client: Discord = self.get_instance(conf.name)
logger.debug(f"[Discord] get_instance('{conf.name}') 返回: {client is not None}")
if client:
client.send_msg(title=message.title, text=message.text,
logger.debug(f"[Discord] 调用 client.send_msg, userid={userid}, title={message.title[:50] if message.title else None}...")
result = client.send_msg(title=message.title, text=message.text,
image=message.image, userid=userid, link=message.link,
buttons=message.buttons,
original_message_id=message.original_message_id,
original_chat_id=message.original_chat_id,
mtype=message.mtype)
logger.debug(f"[Discord] send_msg 返回结果: {result}")
else:
logger.warning(f"[Discord] 未找到配置 '{conf.name}' 对应的 Discord 客户端实例")
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
"""

View File

@@ -2,6 +2,7 @@ import asyncio
import re
import threading
from typing import Optional, List, Dict, Any, Tuple, Union
from urllib.parse import quote
import discord
from discord import app_commands
@@ -33,6 +34,9 @@ class Discord:
DISCORD_GUILD_ID: Optional[Union[str, int]] = None,
DISCORD_CHANNEL_ID: Optional[Union[str, int]] = None,
**kwargs):
logger.debug(f"[Discord] 初始化 Discord 实例: name={kwargs.get('name')}, "
f"GUILD_ID={DISCORD_GUILD_ID}, CHANNEL_ID={DISCORD_CHANNEL_ID}, "
f"TOKEN={'已配置' if DISCORD_BOT_TOKEN else '未配置'}")
if not DISCORD_BOT_TOKEN:
logger.error("Discord Bot Token 未配置!")
return
@@ -40,10 +44,14 @@ class Discord:
self._token = DISCORD_BOT_TOKEN
self._guild_id = self._to_int(DISCORD_GUILD_ID)
self._channel_id = self._to_int(DISCORD_CHANNEL_ID)
logger.debug(f"[Discord] 解析后的 ID: _guild_id={self._guild_id}, _channel_id={self._channel_id}")
base_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message/"
self._ds_url = f"{base_ds_url}?token={settings.API_TOKEN}"
if kwargs.get("name"):
self._ds_url = f"{self._ds_url}&source={kwargs.get('name')}"
# URL encode the source name to handle special characters in config names
encoded_name = quote(kwargs.get('name'), safe='')
self._ds_url = f"{self._ds_url}&source={encoded_name}"
logger.debug(f"[Discord] 消息回调 URL: {self._ds_url}")
intents = discord.Intents.default()
intents.message_content = True
@@ -59,6 +67,7 @@ class Discord:
self._thread: Optional[threading.Thread] = None
self._ready_event = threading.Event()
self._user_dm_cache: Dict[str, discord.DMChannel] = {}
self._user_chat_mapping: Dict[str, str] = {} # userid -> chat_id mapping for reply targeting
self._broadcast_channel = None
self._bot_user_id: Optional[int] = None
@@ -86,6 +95,9 @@ class Discord:
if not self._should_process_message(message):
return
# Update user-chat mapping for reply targeting
self._update_user_chat_mapping(str(message.author.id), str(message.channel.id))
cleaned_text = self._clean_bot_mention(message.content or "")
username = message.author.display_name or message.author.global_name or message.author.name
payload = {
@@ -112,6 +124,10 @@ class Discord:
except Exception as e:
logger.error(f"处理 Discord 交互响应失败:{e}")
# Update user-chat mapping for reply targeting
if interaction.user and interaction.channel:
self._update_user_chat_mapping(str(interaction.user.id), str(interaction.channel.id))
username = (interaction.user.display_name or interaction.user.global_name or interaction.user.name) \
if interaction.user else None
payload = {
@@ -168,13 +184,19 @@ class Discord:
original_message_id: Optional[Union[int, str]] = None,
original_chat_id: Optional[str] = None,
mtype: Optional['NotificationType'] = None) -> Optional[bool]:
logger.debug(f"[Discord] send_msg 被调用: userid={userid}, title={title[:50] if title else None}...")
logger.debug(f"[Discord] get_state() = {self.get_state()}, "
f"_ready_event.is_set() = {self._ready_event.is_set()}, "
f"_client = {self._client is not None}")
if not self.get_state():
logger.warning("[Discord] get_state() 返回 FalseBot 未就绪,无法发送消息")
return False
if not title and not text:
logger.warn("标题和内容不能同时为空")
return False
try:
logger.debug(f"[Discord] 准备异步发送消息...")
future = asyncio.run_coroutine_threadsafe(
self._send_message(title=title, text=text, image=image, userid=userid,
link=link, buttons=buttons,
@@ -182,7 +204,9 @@ class Discord:
original_chat_id=original_chat_id,
mtype=mtype),
self._loop)
return future.result(timeout=30)
result = future.result(timeout=30)
logger.debug(f"[Discord] 异步发送完成,结果: {result}")
return result
except Exception as err:
logger.error(f"发送 Discord 消息失败:{err}")
return False
@@ -254,7 +278,9 @@ class Discord:
original_message_id: Optional[Union[int, str]],
original_chat_id: Optional[str],
mtype: Optional['NotificationType'] = None) -> bool:
logger.debug(f"[Discord] _send_message: userid={userid}, original_chat_id={original_chat_id}")
channel = await self._resolve_channel(userid=userid, chat_id=original_chat_id)
logger.debug(f"[Discord] _resolve_channel 返回: {channel}, type={type(channel)}")
if not channel:
logger.error("未找到可用的 Discord 频道或私聊")
return False
@@ -264,11 +290,18 @@ class Discord:
content = None
if original_message_id and original_chat_id:
logger.debug(f"[Discord] 编辑现有消息: message_id={original_message_id}")
return await self._edit_message(chat_id=original_chat_id, message_id=original_message_id,
content=content, embed=embed, view=view)
await channel.send(content=content, embed=embed, view=view)
return True
logger.debug(f"[Discord] 发送新消息到频道: {channel}")
try:
await channel.send(content=content, embed=embed, view=view)
logger.debug("[Discord] 消息发送成功")
return True
except Exception as e:
logger.error(f"[Discord] 发送消息到频道失败: {e}")
return False
async def _send_list_message(self, embeds: List[discord.Embed],
userid: Optional[str],
@@ -515,26 +548,54 @@ class Discord:
return view
async def _resolve_channel(self, userid: Optional[str] = None, chat_id: Optional[str] = None):
# 优先使用明确的聊天 ID
"""
Resolve the channel to send messages to.
Priority order:
1. `chat_id` (original channel where user sent the message) - for contextual replies
2. `userid` mapping (channel where user last sent a message) - for contextual replies
3. Configured `_channel_id` (broadcast channel) - for system notifications
4. Any available text channel in configured guild - fallback
5. `userid` (DM) - for private conversations as a final fallback
"""
logger.debug(f"[Discord] _resolve_channel: userid={userid}, chat_id={chat_id}, "
f"_channel_id={self._channel_id}, _guild_id={self._guild_id}")
# Priority 1: Use explicit chat_id (reply to the same channel where user sent message)
if chat_id:
logger.debug(f"[Discord] 尝试通过 chat_id={chat_id} 获取原始频道")
channel = self._client.get_channel(int(chat_id))
if channel:
logger.debug(f"[Discord] 通过 get_channel 找到频道: {channel}")
return channel
try:
return await self._client.fetch_channel(int(chat_id))
channel = await self._client.fetch_channel(int(chat_id))
logger.debug(f"[Discord] 通过 fetch_channel 找到频道: {channel}")
return channel
except Exception as err:
logger.warn(f"通过 chat_id 获取 Discord 频道失败:{err}")
# 私聊
# Priority 2: Use user-chat mapping (reply to where the user last sent a message)
if userid:
dm = await self._get_dm_channel(str(userid))
if dm:
return dm
mapped_chat_id = self._get_user_chat_id(str(userid))
if mapped_chat_id:
logger.debug(f"[Discord] 从用户映射获取 chat_id={mapped_chat_id}")
channel = self._client.get_channel(int(mapped_chat_id))
if channel:
logger.debug(f"[Discord] 通过映射找到频道: {channel}")
return channel
try:
channel = await self._client.fetch_channel(int(mapped_chat_id))
logger.debug(f"[Discord] 通过 fetch_channel 找到映射频道: {channel}")
return channel
except Exception as err:
logger.warn(f"通过映射的 chat_id 获取 Discord 频道失败:{err}")
# 配置的广播频道
# Priority 3: Use configured broadcast channel (for system notifications)
if self._broadcast_channel:
logger.debug(f"[Discord] 使用缓存的广播频道: {self._broadcast_channel}")
return self._broadcast_channel
if self._channel_id:
logger.debug(f"[Discord] 尝试通过配置的 _channel_id={self._channel_id} 获取频道")
channel = self._client.get_channel(self._channel_id)
if not channel:
try:
@@ -544,9 +605,11 @@ class Discord:
channel = None
self._broadcast_channel = channel
if channel:
logger.debug(f"[Discord] 通过配置的频道ID找到频道: {channel}")
return channel
# 按 Guild 寻找一个可用文本频道
# Priority 4: Find any available text channel in guild (fallback)
logger.debug(f"[Discord] 尝试在 Guild 中寻找可用频道")
target_guilds = []
if self._guild_id:
guild = self._client.get_guild(self._guild_id)
@@ -554,22 +617,47 @@ class Discord:
target_guilds.append(guild)
else:
target_guilds = list(self._client.guilds)
logger.debug(f"[Discord] 目标 Guilds 数量: {len(target_guilds)}")
for guild in target_guilds:
for channel in guild.text_channels:
if guild.me and channel.permissions_for(guild.me).send_messages:
logger.debug(f"[Discord] 在 Guild 中找到可用频道: {channel}")
self._broadcast_channel = channel
return channel
# Priority 5: Fallback to DM (only if no channel available)
if userid:
logger.debug(f"[Discord] 回退到私聊: userid={userid}")
dm = await self._get_dm_channel(str(userid))
if dm:
logger.debug(f"[Discord] 获取到私聊频道: {dm}")
return dm
else:
logger.debug(f"[Discord] 无法获取用户 {userid} 的私聊频道")
return None
async def _get_dm_channel(self, userid: str) -> Optional[discord.DMChannel]:
logger.debug(f"[Discord] _get_dm_channel: userid={userid}")
if userid in self._user_dm_cache:
logger.debug(f"[Discord] 从缓存获取私聊频道: {self._user_dm_cache.get(userid)}")
return self._user_dm_cache.get(userid)
try:
user_obj = self._client.get_user(int(userid)) or await self._client.fetch_user(int(userid))
logger.debug(f"[Discord] 尝试获取/创建用户 {userid} 的私聊频道")
user_obj = self._client.get_user(int(userid))
logger.debug(f"[Discord] get_user 结果: {user_obj}")
if not user_obj:
user_obj = await self._client.fetch_user(int(userid))
logger.debug(f"[Discord] fetch_user 结果: {user_obj}")
if not user_obj:
logger.debug(f"[Discord] 无法找到用户 {userid}")
return None
dm = user_obj.dm_channel or await user_obj.create_dm()
dm = user_obj.dm_channel
logger.debug(f"[Discord] 用户现有 dm_channel: {dm}")
if not dm:
dm = await user_obj.create_dm()
logger.debug(f"[Discord] 创建新的 dm_channel: {dm}")
if dm:
self._user_dm_cache[userid] = dm
return dm
@@ -577,6 +665,25 @@ class Discord:
logger.error(f"获取 Discord 私聊失败:{err}")
return None
def _update_user_chat_mapping(self, userid: str, chat_id: str) -> None:
"""
Update user-chat mapping for reply targeting.
This ensures replies go to the same channel where the user sent the message.
:param userid: User ID
:param chat_id: Channel/Chat ID where the user sent the message
"""
if userid and chat_id:
self._user_chat_mapping[userid] = chat_id
logger.debug(f"[Discord] 更新用户频道映射: userid={userid} -> chat_id={chat_id}")
def _get_user_chat_id(self, userid: str) -> Optional[str]:
"""
Get the chat ID where the user last sent a message.
:param userid: User ID
:return: Chat ID or None if not found
"""
return self._user_chat_mapping.get(userid)
def _should_process_message(self, message: discord.Message) -> bool:
if isinstance(message.channel, discord.DMChannel):
return True

View File

@@ -1,6 +1,7 @@
import re
from threading import Lock
from typing import List, Optional
from urllib.parse import quote
import requests
from slack_bolt import App
@@ -42,7 +43,9 @@ class Slack:
# 标记消息来源
if kwargs.get("name"):
self._ds_url = f"{self._ds_url}&source={kwargs.get('name')}"
# URL encode the source name to handle special characters
encoded_name = quote(kwargs.get('name'), safe='')
self._ds_url = f"{self._ds_url}&source={encoded_name}"
# 注册消息响应
@slack_app.event("message")

View File

@@ -2,7 +2,7 @@ import asyncio
import re
import threading
from typing import Optional, List, Dict, Callable
from urllib.parse import urljoin
from urllib.parse import urljoin, quote
from telebot import TeleBot, apihelper
from telebot.types import BotCommand, InlineKeyboardMarkup, InlineKeyboardButton, InputMediaPhoto
@@ -65,7 +65,9 @@ class Telegram:
# 标记渠道来源
if kwargs.get("name"):
self._ds_url = f"{self._ds_url}&source={kwargs.get('name')}"
# URL encode the source name to handle special characters
encoded_name = quote(kwargs.get('name'), safe='')
self._ds_url = f"{self._ds_url}&source={encoded_name}"
@_bot.message_handler(commands=['start', 'help'])
def send_welcome(message):