mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 16:36:53 +08:00
feat(agent): 支持Slack和Discord渠道的流式输出功能
- 为Slack添加MESSAGE_EDITING能力 - 为Slack添加edit_message和send_direct_message方法 - 为Discord添加edit_message和send_direct_message方法 - 修改Discord send_msg返回(bool, message_id)元组以支持流式输出
This commit is contained in:
+167
-41
@@ -4,7 +4,7 @@ from typing import Optional, Union, List, Tuple, Any
|
|||||||
from app.core.context import MediaInfo, Context
|
from app.core.context import MediaInfo, Context
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase, _MessageBase
|
from app.modules import _ModuleBase, _MessageBase
|
||||||
from app.schemas import MessageChannel, CommingMessage, Notification
|
from app.schemas import MessageChannel, CommingMessage, Notification, MessageResponse
|
||||||
from app.schemas.types import ModuleType
|
from app.schemas.types import ModuleType
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -15,7 +15,6 @@ except Exception as err: # ImportError or other load issues
|
|||||||
|
|
||||||
|
|
||||||
class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
||||||
|
|
||||||
def init_module(self) -> None:
|
def init_module(self) -> None:
|
||||||
"""
|
"""
|
||||||
初始化模块
|
初始化模块
|
||||||
@@ -24,8 +23,9 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
logger.error("Discord 依赖未就绪(需要安装 discord.py==2.6.4),模块未启动")
|
logger.error("Discord 依赖未就绪(需要安装 discord.py==2.6.4),模块未启动")
|
||||||
return
|
return
|
||||||
self.stop()
|
self.stop()
|
||||||
super().init_service(service_name=Discord.__name__.lower(),
|
super().init_service(
|
||||||
service_type=Discord)
|
service_name=Discord.__name__.lower(), service_type=Discord
|
||||||
|
)
|
||||||
self._channel = MessageChannel.Discord
|
self._channel = MessageChannel.Discord
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -75,7 +75,9 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def message_parser(self, source: str, body: Any, form: Any, args: Any) -> Optional[CommingMessage]:
|
def message_parser(
|
||||||
|
self, source: str, body: Any, form: Any, args: Any
|
||||||
|
) -> Optional[CommingMessage]:
|
||||||
"""
|
"""
|
||||||
解析消息内容,返回字典,注意以下约定值:
|
解析消息内容,返回字典,注意以下约定值:
|
||||||
userid: 用户ID
|
userid: 用户ID
|
||||||
@@ -108,8 +110,10 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
message_id = msg_json.get("message_id")
|
message_id = msg_json.get("message_id")
|
||||||
chat_id = msg_json.get("chat_id")
|
chat_id = msg_json.get("chat_id")
|
||||||
if callback_data and userid:
|
if callback_data and userid:
|
||||||
logger.info(f"收到来自 {client_config.name} 的 Discord 按钮回调:"
|
logger.info(
|
||||||
f"userid={userid}, username={username}, callback_data={callback_data}")
|
f"收到来自 {client_config.name} 的 Discord 按钮回调:"
|
||||||
|
f"userid={userid}, username={username}, callback_data={callback_data}"
|
||||||
|
)
|
||||||
return CommingMessage(
|
return CommingMessage(
|
||||||
channel=MessageChannel.Discord,
|
channel=MessageChannel.Discord,
|
||||||
source=client_config.name,
|
source=client_config.name,
|
||||||
@@ -119,7 +123,7 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
is_callback=True,
|
is_callback=True,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
chat_id=str(chat_id) if chat_id else None
|
chat_id=str(chat_id) if chat_id else None,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -127,11 +131,18 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
text = msg_json.get("text")
|
text = msg_json.get("text")
|
||||||
chat_id = msg_json.get("chat_id")
|
chat_id = msg_json.get("chat_id")
|
||||||
if text and userid:
|
if text and userid:
|
||||||
logger.info(f"收到来自 {client_config.name} 的 Discord 消息:"
|
logger.info(
|
||||||
f"userid={userid}, username={username}, text={text}")
|
f"收到来自 {client_config.name} 的 Discord 消息:"
|
||||||
return CommingMessage(channel=MessageChannel.Discord, source=client_config.name,
|
f"userid={userid}, username={username}, text={text}"
|
||||||
userid=userid, username=username, text=text,
|
)
|
||||||
chat_id=str(chat_id) if chat_id else None)
|
return CommingMessage(
|
||||||
|
channel=MessageChannel.Discord,
|
||||||
|
source=client_config.name,
|
||||||
|
userid=userid,
|
||||||
|
username=username,
|
||||||
|
text=text,
|
||||||
|
chat_id=str(chat_id) if chat_id else None,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def post_message(self, message: Notification, **kwargs) -> None:
|
def post_message(self, message: Notification, **kwargs) -> None:
|
||||||
@@ -141,43 +152,66 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
"""
|
"""
|
||||||
# DEBUG: Log entry and configs
|
# DEBUG: Log entry and configs
|
||||||
configs = self.get_configs()
|
configs = self.get_configs()
|
||||||
logger.debug(f"[Discord] post_message 被调用,message.source={message.source}, "
|
logger.debug(
|
||||||
f"message.userid={message.userid}, message.channel={message.channel}")
|
f"[Discord] post_message 被调用,message.source={message.source}, "
|
||||||
logger.debug(f"[Discord] 当前配置数量: {len(configs)}, 配置名称: {list(configs.keys())}")
|
f"message.userid={message.userid}, message.channel={message.channel}"
|
||||||
logger.debug(f"[Discord] 当前实例数量: {len(self.get_instances())}, 实例名称: {list(self.get_instances().keys())}")
|
)
|
||||||
|
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:
|
if not configs:
|
||||||
logger.warning("[Discord] get_configs() 返回空,没有可用的 Discord 配置")
|
logger.warning("[Discord] get_configs() 返回空,没有可用的 Discord 配置")
|
||||||
return
|
return
|
||||||
|
|
||||||
for conf in configs.values():
|
for conf in configs.values():
|
||||||
logger.debug(f"[Discord] 检查配置: name={conf.name}, type={conf.type}, enabled={conf.enabled}")
|
logger.debug(
|
||||||
|
f"[Discord] 检查配置: name={conf.name}, type={conf.type}, enabled={conf.enabled}"
|
||||||
|
)
|
||||||
if not self.check_message(message, conf.name):
|
if not self.check_message(message, conf.name):
|
||||||
logger.debug(f"[Discord] check_message 返回 False,跳过配置: {conf.name}")
|
logger.debug(
|
||||||
|
f"[Discord] check_message 返回 False,跳过配置: {conf.name}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
logger.debug(f"[Discord] check_message 通过,准备发送到: {conf.name}")
|
logger.debug(f"[Discord] check_message 通过,准备发送到: {conf.name}")
|
||||||
targets = message.targets
|
targets = message.targets
|
||||||
userid = message.userid
|
userid = message.userid
|
||||||
if not userid and targets is not None:
|
if not userid and targets is not None:
|
||||||
userid = targets.get('discord_userid')
|
userid = targets.get("discord_userid")
|
||||||
if not userid:
|
if not userid:
|
||||||
logger.warn("用户没有指定 Discord 用户ID,消息无法发送")
|
logger.warn("用户没有指定 Discord 用户ID,消息无法发送")
|
||||||
return
|
return
|
||||||
client: Discord = self.get_instance(conf.name)
|
client: Discord = self.get_instance(conf.name)
|
||||||
logger.debug(f"[Discord] get_instance('{conf.name}') 返回: {client is not None}")
|
logger.debug(
|
||||||
|
f"[Discord] get_instance('{conf.name}') 返回: {client is not None}"
|
||||||
|
)
|
||||||
if client:
|
if client:
|
||||||
logger.debug(f"[Discord] 调用 client.send_msg, userid={userid}, title={message.title[:50] if message.title else None}...")
|
logger.debug(
|
||||||
result = client.send_msg(title=message.title, text=message.text,
|
f"[Discord] 调用 client.send_msg, userid={userid}, title={message.title[:50] if message.title else None}..."
|
||||||
image=message.image, userid=userid, link=message.link,
|
)
|
||||||
buttons=message.buttons,
|
result = client.send_msg(
|
||||||
original_message_id=message.original_message_id,
|
title=message.title,
|
||||||
original_chat_id=message.original_chat_id,
|
text=message.text,
|
||||||
mtype=message.mtype)
|
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}")
|
logger.debug(f"[Discord] send_msg 返回结果: {result}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[Discord] 未找到配置 '{conf.name}' 对应的 Discord 客户端实例")
|
logger.warning(
|
||||||
|
f"[Discord] 未找到配置 '{conf.name}' 对应的 Discord 客户端实例"
|
||||||
|
)
|
||||||
|
|
||||||
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
|
def post_medias_message(
|
||||||
|
self, message: Notification, medias: List[MediaInfo]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
发送媒体信息选择列表
|
发送媒体信息选择列表
|
||||||
:param message: 消息体
|
:param message: 消息体
|
||||||
@@ -189,12 +223,18 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
continue
|
continue
|
||||||
client: Discord = self.get_instance(conf.name)
|
client: Discord = self.get_instance(conf.name)
|
||||||
if client:
|
if client:
|
||||||
client.send_medias_msg(title=message.title, medias=medias, userid=message.userid,
|
client.send_medias_msg(
|
||||||
buttons=message.buttons,
|
title=message.title,
|
||||||
original_message_id=message.original_message_id,
|
medias=medias,
|
||||||
original_chat_id=message.original_chat_id)
|
userid=message.userid,
|
||||||
|
buttons=message.buttons,
|
||||||
|
original_message_id=message.original_message_id,
|
||||||
|
original_chat_id=message.original_chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None:
|
def post_torrents_message(
|
||||||
|
self, message: Notification, torrents: List[Context]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
发送种子信息选择列表
|
发送种子信息选择列表
|
||||||
:param message: 消息体
|
:param message: 消息体
|
||||||
@@ -206,13 +246,22 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
continue
|
continue
|
||||||
client: Discord = self.get_instance(conf.name)
|
client: Discord = self.get_instance(conf.name)
|
||||||
if client:
|
if client:
|
||||||
client.send_torrents_msg(title=message.title, torrents=torrents,
|
client.send_torrents_msg(
|
||||||
userid=message.userid, buttons=message.buttons,
|
title=message.title,
|
||||||
original_message_id=message.original_message_id,
|
torrents=torrents,
|
||||||
original_chat_id=message.original_chat_id)
|
userid=message.userid,
|
||||||
|
buttons=message.buttons,
|
||||||
|
original_message_id=message.original_message_id,
|
||||||
|
original_chat_id=message.original_chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
def delete_message(self, channel: MessageChannel, source: str,
|
def delete_message(
|
||||||
message_id: str, chat_id: Optional[str] = None) -> bool:
|
self,
|
||||||
|
channel: MessageChannel,
|
||||||
|
source: str,
|
||||||
|
message_id: str,
|
||||||
|
chat_id: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
删除消息
|
删除消息
|
||||||
:param channel: 消息渠道
|
:param channel: 消息渠道
|
||||||
@@ -233,3 +282,80 @@ class DiscordModule(_ModuleBase, _MessageBase[Discord]):
|
|||||||
if result:
|
if result:
|
||||||
success = True
|
success = True
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
def edit_message(
|
||||||
|
self,
|
||||||
|
channel: MessageChannel,
|
||||||
|
source: str,
|
||||||
|
message_id: Union[str, int],
|
||||||
|
chat_id: Union[str, int],
|
||||||
|
text: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
编辑消息
|
||||||
|
:param channel: 消息渠道
|
||||||
|
:param source: 指定的消息源
|
||||||
|
:param message_id: 消息ID
|
||||||
|
:param chat_id: 聊天ID
|
||||||
|
:param text: 新的消息内容
|
||||||
|
:param title: 消息标题
|
||||||
|
:return: 编辑是否成功
|
||||||
|
"""
|
||||||
|
if channel != self._channel:
|
||||||
|
return False
|
||||||
|
for conf in self.get_configs().values():
|
||||||
|
if source != conf.name:
|
||||||
|
continue
|
||||||
|
client: Discord = self.get_instance(conf.name)
|
||||||
|
if client:
|
||||||
|
result = client.send_msg(
|
||||||
|
title=title or "",
|
||||||
|
text=text,
|
||||||
|
original_message_id=message_id,
|
||||||
|
original_chat_id=str(chat_id),
|
||||||
|
)
|
||||||
|
if result and isinstance(result, tuple) and result[0]:
|
||||||
|
return True
|
||||||
|
elif result:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_direct_message(self, message: Notification) -> Optional[MessageResponse]:
|
||||||
|
"""
|
||||||
|
直接发送消息并返回消息ID等信息
|
||||||
|
:param message: 消息体
|
||||||
|
:return: 消息响应(包含message_id, chat_id等)
|
||||||
|
"""
|
||||||
|
for conf in self.get_configs().values():
|
||||||
|
if not self.check_message(message, conf.name):
|
||||||
|
continue
|
||||||
|
targets = message.targets
|
||||||
|
userid = message.userid
|
||||||
|
if not userid and targets is not None:
|
||||||
|
userid = targets.get("discord_userid")
|
||||||
|
if not userid:
|
||||||
|
logger.warn("用户没有指定 Discord 用户ID,消息无法发送")
|
||||||
|
return None
|
||||||
|
client: Discord = self.get_instance(conf.name)
|
||||||
|
if client:
|
||||||
|
result = client.send_msg(
|
||||||
|
title=message.title or "",
|
||||||
|
text=message.text,
|
||||||
|
userid=userid,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
success, message_id = (
|
||||||
|
(result[0], result[1])
|
||||||
|
if isinstance(result, tuple)
|
||||||
|
else (result, None)
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
return MessageResponse(
|
||||||
|
message_id=str(message_id) if message_id else None,
|
||||||
|
chat_id=None,
|
||||||
|
channel=MessageChannel.Discord,
|
||||||
|
source=conf.name,
|
||||||
|
success=True,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|||||||
+272
-121
@@ -18,10 +18,10 @@ from app.utils.string import StringUtils
|
|||||||
# Discord embed 字段解析白名单
|
# Discord embed 字段解析白名单
|
||||||
# 只有这些消息类型会使用复杂的字段解析逻辑
|
# 只有这些消息类型会使用复杂的字段解析逻辑
|
||||||
PARSE_FIELD_TYPES = {
|
PARSE_FIELD_TYPES = {
|
||||||
NotificationType.Download, # 资源下载
|
NotificationType.Download, # 资源下载
|
||||||
NotificationType.Organize, # 整理入库
|
NotificationType.Organize, # 整理入库
|
||||||
NotificationType.Subscribe, # 订阅
|
NotificationType.Subscribe, # 订阅
|
||||||
NotificationType.Manual, # 手动处理
|
NotificationType.Manual, # 手动处理
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -30,13 +30,18 @@ class Discord:
|
|||||||
Discord Bot 通知与交互实现(基于 discord.py 2.6.4)
|
Discord Bot 通知与交互实现(基于 discord.py 2.6.4)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, DISCORD_BOT_TOKEN: Optional[str] = None,
|
def __init__(
|
||||||
DISCORD_GUILD_ID: Optional[Union[str, int]] = None,
|
self,
|
||||||
DISCORD_CHANNEL_ID: Optional[Union[str, int]] = None,
|
DISCORD_BOT_TOKEN: Optional[str] = None,
|
||||||
**kwargs):
|
DISCORD_GUILD_ID: Optional[Union[str, int]] = None,
|
||||||
logger.debug(f"[Discord] 初始化 Discord 实例: name={kwargs.get('name')}, "
|
DISCORD_CHANNEL_ID: Optional[Union[str, int]] = None,
|
||||||
f"GUILD_ID={DISCORD_GUILD_ID}, CHANNEL_ID={DISCORD_CHANNEL_ID}, "
|
**kwargs,
|
||||||
f"TOKEN={'已配置' if DISCORD_BOT_TOKEN else '未配置'}")
|
):
|
||||||
|
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:
|
if not DISCORD_BOT_TOKEN:
|
||||||
logger.error("Discord Bot Token 未配置!")
|
logger.error("Discord Bot Token 未配置!")
|
||||||
return
|
return
|
||||||
@@ -44,12 +49,14 @@ class Discord:
|
|||||||
self._token = DISCORD_BOT_TOKEN
|
self._token = DISCORD_BOT_TOKEN
|
||||||
self._guild_id = self._to_int(DISCORD_GUILD_ID)
|
self._guild_id = self._to_int(DISCORD_GUILD_ID)
|
||||||
self._channel_id = self._to_int(DISCORD_CHANNEL_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}")
|
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/"
|
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}"
|
self._ds_url = f"{base_ds_url}?token={settings.API_TOKEN}"
|
||||||
if kwargs.get("name"):
|
if kwargs.get("name"):
|
||||||
# URL encode the source name to handle special characters in config names
|
# URL encode the source name to handle special characters in config names
|
||||||
encoded_name = quote(kwargs.get('name'), safe='')
|
encoded_name = quote(kwargs.get("name"), safe="")
|
||||||
self._ds_url = f"{self._ds_url}&source={encoded_name}"
|
self._ds_url = f"{self._ds_url}&source={encoded_name}"
|
||||||
logger.debug(f"[Discord] 消息回调 URL: {self._ds_url}")
|
logger.debug(f"[Discord] 消息回调 URL: {self._ds_url}")
|
||||||
|
|
||||||
@@ -59,15 +66,16 @@ class Discord:
|
|||||||
intents.guilds = True
|
intents.guilds = True
|
||||||
|
|
||||||
self._client: Optional[discord.Client] = discord.Client(
|
self._client: Optional[discord.Client] = discord.Client(
|
||||||
intents=intents,
|
intents=intents, proxy=settings.PROXY_HOST
|
||||||
proxy=settings.PROXY_HOST
|
|
||||||
)
|
)
|
||||||
self._tree: Optional[app_commands.CommandTree] = None
|
self._tree: Optional[app_commands.CommandTree] = None
|
||||||
self._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop()
|
self._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop()
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: Optional[threading.Thread] = None
|
||||||
self._ready_event = threading.Event()
|
self._ready_event = threading.Event()
|
||||||
self._user_dm_cache: Dict[str, discord.DMChannel] = {}
|
self._user_dm_cache: Dict[str, discord.DMChannel] = {}
|
||||||
self._user_chat_mapping: Dict[str, str] = {} # userid -> chat_id mapping for reply targeting
|
self._user_chat_mapping: Dict[
|
||||||
|
str, str
|
||||||
|
] = {} # userid -> chat_id mapping for reply targeting
|
||||||
self._broadcast_channel = None
|
self._broadcast_channel = None
|
||||||
self._bot_user_id: Optional[int] = None
|
self._bot_user_id: Optional[int] = None
|
||||||
|
|
||||||
@@ -96,10 +104,16 @@ class Discord:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Update user-chat mapping for reply targeting
|
# Update user-chat mapping for reply targeting
|
||||||
self._update_user_chat_mapping(str(message.author.id), str(message.channel.id))
|
self._update_user_chat_mapping(
|
||||||
|
str(message.author.id), str(message.channel.id)
|
||||||
|
)
|
||||||
|
|
||||||
cleaned_text = self._clean_bot_mention(message.content or "")
|
cleaned_text = self._clean_bot_mention(message.content or "")
|
||||||
username = message.author.display_name or message.author.global_name or message.author.name
|
username = (
|
||||||
|
message.author.display_name
|
||||||
|
or message.author.global_name
|
||||||
|
or message.author.name
|
||||||
|
)
|
||||||
payload = {
|
payload = {
|
||||||
"type": "message",
|
"type": "message",
|
||||||
"userid": str(message.author.id),
|
"userid": str(message.author.id),
|
||||||
@@ -108,7 +122,9 @@ class Discord:
|
|||||||
"text": cleaned_text,
|
"text": cleaned_text,
|
||||||
"message_id": str(message.id),
|
"message_id": str(message.id),
|
||||||
"chat_id": str(message.channel.id),
|
"chat_id": str(message.channel.id),
|
||||||
"channel_type": "dm" if isinstance(message.channel, discord.DMChannel) else "guild"
|
"channel_type": "dm"
|
||||||
|
if isinstance(message.channel, discord.DMChannel)
|
||||||
|
else "guild",
|
||||||
}
|
}
|
||||||
await self._post_to_ds(payload)
|
await self._post_to_ds(payload)
|
||||||
|
|
||||||
@@ -126,18 +142,31 @@ class Discord:
|
|||||||
|
|
||||||
# Update user-chat mapping for reply targeting
|
# Update user-chat mapping for reply targeting
|
||||||
if interaction.user and interaction.channel:
|
if interaction.user and interaction.channel:
|
||||||
self._update_user_chat_mapping(str(interaction.user.id), str(interaction.channel.id))
|
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) \
|
username = (
|
||||||
if interaction.user else None
|
(
|
||||||
|
interaction.user.display_name
|
||||||
|
or interaction.user.global_name
|
||||||
|
or interaction.user.name
|
||||||
|
)
|
||||||
|
if interaction.user
|
||||||
|
else None
|
||||||
|
)
|
||||||
payload = {
|
payload = {
|
||||||
"type": "interaction",
|
"type": "interaction",
|
||||||
"userid": str(interaction.user.id) if interaction.user else None,
|
"userid": str(interaction.user.id) if interaction.user else None,
|
||||||
"username": username,
|
"username": username,
|
||||||
"user_tag": str(interaction.user) if interaction.user else None,
|
"user_tag": str(interaction.user) if interaction.user else None,
|
||||||
"callback_data": callback_data,
|
"callback_data": callback_data,
|
||||||
"message_id": str(interaction.message.id) if interaction.message else None,
|
"message_id": str(interaction.message.id)
|
||||||
"chat_id": str(interaction.channel.id) if interaction.channel else None
|
if interaction.message
|
||||||
|
else None,
|
||||||
|
"chat_id": str(interaction.channel.id)
|
||||||
|
if interaction.channel
|
||||||
|
else None,
|
||||||
}
|
}
|
||||||
await self._post_to_ds(payload)
|
await self._post_to_ds(payload)
|
||||||
|
|
||||||
@@ -165,7 +194,9 @@ class Discord:
|
|||||||
if not self._client or not self._loop or not self._thread:
|
if not self._client or not self._loop or not self._thread:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
asyncio.run_coroutine_threadsafe(self._client.close(), self._loop).result(timeout=10)
|
asyncio.run_coroutine_threadsafe(self._client.close(), self._loop).result(
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"关闭 Discord Bot 失败:{err}")
|
logger.error(f"关闭 Discord Bot 失败:{err}")
|
||||||
finally:
|
finally:
|
||||||
@@ -178,16 +209,26 @@ class Discord:
|
|||||||
def get_state(self) -> bool:
|
def get_state(self) -> bool:
|
||||||
return self._ready_event.is_set() and self._client is not None
|
return self._ready_event.is_set() and self._client is not None
|
||||||
|
|
||||||
def send_msg(self, title: str, text: Optional[str] = None, image: Optional[str] = None,
|
def send_msg(
|
||||||
userid: Optional[str] = None, link: Optional[str] = None,
|
self,
|
||||||
buttons: Optional[List[List[dict]]] = None,
|
title: str,
|
||||||
original_message_id: Optional[Union[int, str]] = None,
|
text: Optional[str] = None,
|
||||||
original_chat_id: Optional[str] = None,
|
image: Optional[str] = None,
|
||||||
mtype: Optional['NotificationType'] = None) -> Optional[bool]:
|
userid: Optional[str] = None,
|
||||||
logger.debug(f"[Discord] send_msg 被调用: userid={userid}, title={title[:50] if title else None}...")
|
link: Optional[str] = None,
|
||||||
logger.debug(f"[Discord] get_state() = {self.get_state()}, "
|
buttons: Optional[List[List[dict]]] = None,
|
||||||
f"_ready_event.is_set() = {self._ready_event.is_set()}, "
|
original_message_id: Optional[Union[int, str]] = None,
|
||||||
f"_client = {self._client is not 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():
|
if not self.get_state():
|
||||||
logger.warning("[Discord] get_state() 返回 False,Bot 未就绪,无法发送消息")
|
logger.warning("[Discord] get_state() 返回 False,Bot 未就绪,无法发送消息")
|
||||||
return False
|
return False
|
||||||
@@ -198,12 +239,19 @@ class Discord:
|
|||||||
try:
|
try:
|
||||||
logger.debug(f"[Discord] 准备异步发送消息...")
|
logger.debug(f"[Discord] 准备异步发送消息...")
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self._send_message(title=title, text=text, image=image, userid=userid,
|
self._send_message(
|
||||||
link=link, buttons=buttons,
|
title=title,
|
||||||
original_message_id=original_message_id,
|
text=text,
|
||||||
original_chat_id=original_chat_id,
|
image=image,
|
||||||
mtype=mtype),
|
userid=userid,
|
||||||
self._loop)
|
link=link,
|
||||||
|
buttons=buttons,
|
||||||
|
original_message_id=original_message_id,
|
||||||
|
original_chat_id=original_chat_id,
|
||||||
|
mtype=mtype,
|
||||||
|
),
|
||||||
|
self._loop,
|
||||||
|
)
|
||||||
result = future.result(timeout=30)
|
result = future.result(timeout=30)
|
||||||
logger.debug(f"[Discord] 异步发送完成,结果: {result}")
|
logger.debug(f"[Discord] 异步发送完成,结果: {result}")
|
||||||
return result
|
return result
|
||||||
@@ -211,10 +259,15 @@ class Discord:
|
|||||||
logger.error(f"发送 Discord 消息失败:{err}")
|
logger.error(f"发送 Discord 消息失败:{err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def send_medias_msg(self, medias: List[MediaInfo], userid: Optional[str] = None, title: Optional[str] = None,
|
def send_medias_msg(
|
||||||
buttons: Optional[List[List[dict]]] = None,
|
self,
|
||||||
original_message_id: Optional[Union[int, str]] = None,
|
medias: List[MediaInfo],
|
||||||
original_chat_id: Optional[str] = None) -> Optional[bool]:
|
userid: Optional[str] = None,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
buttons: Optional[List[List[dict]]] = None,
|
||||||
|
original_message_id: Optional[Union[int, str]] = None,
|
||||||
|
original_chat_id: Optional[str] = None,
|
||||||
|
) -> Optional[bool]:
|
||||||
if not self.get_state() or not medias:
|
if not self.get_state() or not medias:
|
||||||
return False
|
return False
|
||||||
title = title or "媒体列表"
|
title = title or "媒体列表"
|
||||||
@@ -223,22 +276,29 @@ class Discord:
|
|||||||
self._send_list_message(
|
self._send_list_message(
|
||||||
embeds=self._build_media_embeds(medias, title),
|
embeds=self._build_media_embeds(medias, title),
|
||||||
userid=userid,
|
userid=userid,
|
||||||
buttons=self._build_default_buttons(len(medias)) if not buttons else buttons,
|
buttons=self._build_default_buttons(len(medias))
|
||||||
|
if not buttons
|
||||||
|
else buttons,
|
||||||
fallback_buttons=buttons,
|
fallback_buttons=buttons,
|
||||||
original_message_id=original_message_id,
|
original_message_id=original_message_id,
|
||||||
original_chat_id=original_chat_id
|
original_chat_id=original_chat_id,
|
||||||
),
|
),
|
||||||
self._loop
|
self._loop,
|
||||||
)
|
)
|
||||||
return future.result(timeout=30)
|
return future.result(timeout=30)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"发送 Discord 媒体列表失败:{err}")
|
logger.error(f"发送 Discord 媒体列表失败:{err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def send_torrents_msg(self, torrents: List[Context], userid: Optional[str] = None, title: Optional[str] = None,
|
def send_torrents_msg(
|
||||||
buttons: Optional[List[List[dict]]] = None,
|
self,
|
||||||
original_message_id: Optional[Union[int, str]] = None,
|
torrents: List[Context],
|
||||||
original_chat_id: Optional[str] = None) -> Optional[bool]:
|
userid: Optional[str] = None,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
buttons: Optional[List[List[dict]]] = None,
|
||||||
|
original_message_id: Optional[Union[int, str]] = None,
|
||||||
|
original_chat_id: Optional[str] = None,
|
||||||
|
) -> Optional[bool]:
|
||||||
if not self.get_state() or not torrents:
|
if not self.get_state() or not torrents:
|
||||||
return False
|
return False
|
||||||
title = title or "种子列表"
|
title = title or "种子列表"
|
||||||
@@ -247,68 +307,92 @@ class Discord:
|
|||||||
self._send_list_message(
|
self._send_list_message(
|
||||||
embeds=self._build_torrent_embeds(torrents, title),
|
embeds=self._build_torrent_embeds(torrents, title),
|
||||||
userid=userid,
|
userid=userid,
|
||||||
buttons=self._build_default_buttons(len(torrents)) if not buttons else buttons,
|
buttons=self._build_default_buttons(len(torrents))
|
||||||
|
if not buttons
|
||||||
|
else buttons,
|
||||||
fallback_buttons=buttons,
|
fallback_buttons=buttons,
|
||||||
original_message_id=original_message_id,
|
original_message_id=original_message_id,
|
||||||
original_chat_id=original_chat_id
|
original_chat_id=original_chat_id,
|
||||||
),
|
),
|
||||||
self._loop
|
self._loop,
|
||||||
)
|
)
|
||||||
return future.result(timeout=30)
|
return future.result(timeout=30)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"发送 Discord 种子列表失败:{err}")
|
logger.error(f"发送 Discord 种子列表失败:{err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_msg(self, message_id: Union[str, int], chat_id: Optional[str] = None) -> Optional[bool]:
|
def delete_msg(
|
||||||
|
self, message_id: Union[str, int], chat_id: Optional[str] = None
|
||||||
|
) -> Optional[bool]:
|
||||||
if not self.get_state():
|
if not self.get_state():
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self._delete_message(message_id=message_id, chat_id=chat_id),
|
self._delete_message(message_id=message_id, chat_id=chat_id), self._loop
|
||||||
self._loop
|
|
||||||
)
|
)
|
||||||
return future.result(timeout=15)
|
return future.result(timeout=15)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"删除 Discord 消息失败:{err}")
|
logger.error(f"删除 Discord 消息失败:{err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _send_message(self, title: str, text: Optional[str], image: Optional[str],
|
async def _send_message(
|
||||||
userid: Optional[str], link: Optional[str],
|
self,
|
||||||
buttons: Optional[List[List[dict]]],
|
title: str,
|
||||||
original_message_id: Optional[Union[int, str]],
|
text: Optional[str],
|
||||||
original_chat_id: Optional[str],
|
image: Optional[str],
|
||||||
mtype: Optional['NotificationType'] = None) -> bool:
|
userid: Optional[str],
|
||||||
logger.debug(f"[Discord] _send_message: userid={userid}, original_chat_id={original_chat_id}")
|
link: Optional[str],
|
||||||
|
buttons: Optional[List[List[dict]]],
|
||||||
|
original_message_id: Optional[Union[int, str]],
|
||||||
|
original_chat_id: Optional[str],
|
||||||
|
mtype: Optional["NotificationType"] = None,
|
||||||
|
) -> Tuple[bool, Optional[int]]:
|
||||||
|
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)
|
channel = await self._resolve_channel(userid=userid, chat_id=original_chat_id)
|
||||||
logger.debug(f"[Discord] _resolve_channel 返回: {channel}, type={type(channel)}")
|
logger.debug(
|
||||||
|
f"[Discord] _resolve_channel 返回: {channel}, type={type(channel)}"
|
||||||
|
)
|
||||||
if not channel:
|
if not channel:
|
||||||
logger.error("未找到可用的 Discord 频道或私聊")
|
logger.error("未找到可用的 Discord 频道或私聊")
|
||||||
return False
|
return False, None
|
||||||
|
|
||||||
embed = self._build_embed(title=title, text=text, image=image, link=link, mtype=mtype)
|
embed = self._build_embed(
|
||||||
|
title=title, text=text, image=image, link=link, mtype=mtype
|
||||||
|
)
|
||||||
view = self._build_view(buttons=buttons, link=link)
|
view = self._build_view(buttons=buttons, link=link)
|
||||||
content = None
|
content = None
|
||||||
|
|
||||||
if original_message_id and original_chat_id:
|
if original_message_id and original_chat_id:
|
||||||
logger.debug(f"[Discord] 编辑现有消息: message_id={original_message_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,
|
success = await self._edit_message(
|
||||||
content=content, embed=embed, view=view)
|
chat_id=original_chat_id,
|
||||||
|
message_id=original_message_id,
|
||||||
|
content=content,
|
||||||
|
embed=embed,
|
||||||
|
view=view,
|
||||||
|
)
|
||||||
|
return success, int(original_message_id) if original_message_id else None
|
||||||
|
|
||||||
logger.debug(f"[Discord] 发送新消息到频道: {channel}")
|
logger.debug(f"[Discord] 发送新消息到频道: {channel}")
|
||||||
try:
|
try:
|
||||||
await channel.send(content=content, embed=embed, view=view)
|
sent_message = await channel.send(content=content, embed=embed, view=view)
|
||||||
logger.debug("[Discord] 消息发送成功")
|
logger.debug("[Discord] 消息发送成功")
|
||||||
return True
|
return True, sent_message.id if sent_message else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[Discord] 发送消息到频道失败: {e}")
|
logger.error(f"[Discord] 发送消息到频道失败: {e}")
|
||||||
return False
|
return False, None
|
||||||
|
|
||||||
async def _send_list_message(self, embeds: List[discord.Embed],
|
async def _send_list_message(
|
||||||
userid: Optional[str],
|
self,
|
||||||
buttons: Optional[List[List[dict]]],
|
embeds: List[discord.Embed],
|
||||||
fallback_buttons: Optional[List[List[dict]]],
|
userid: Optional[str],
|
||||||
original_message_id: Optional[Union[int, str]],
|
buttons: Optional[List[List[dict]]],
|
||||||
original_chat_id: Optional[str]) -> bool:
|
fallback_buttons: Optional[List[List[dict]]],
|
||||||
|
original_message_id: Optional[Union[int, str]],
|
||||||
|
original_chat_id: Optional[str],
|
||||||
|
) -> bool:
|
||||||
channel = await self._resolve_channel(userid=userid, chat_id=original_chat_id)
|
channel = await self._resolve_channel(userid=userid, chat_id=original_chat_id)
|
||||||
if not channel:
|
if not channel:
|
||||||
logger.error("未找到可用的 Discord 频道或私聊")
|
logger.error("未找到可用的 Discord 频道或私聊")
|
||||||
@@ -318,17 +402,31 @@ class Discord:
|
|||||||
embeds = embeds[:10] if embeds else [] # Discord 单条消息最多 10 个 embed
|
embeds = embeds[:10] if embeds else [] # Discord 单条消息最多 10 个 embed
|
||||||
|
|
||||||
if original_message_id and original_chat_id:
|
if original_message_id and original_chat_id:
|
||||||
return await self._edit_message(chat_id=original_chat_id, message_id=original_message_id,
|
return await self._edit_message(
|
||||||
content=None, embed=None, view=view, embeds=embeds)
|
chat_id=original_chat_id,
|
||||||
|
message_id=original_message_id,
|
||||||
|
content=None,
|
||||||
|
embed=None,
|
||||||
|
view=view,
|
||||||
|
embeds=embeds,
|
||||||
|
)
|
||||||
|
|
||||||
await channel.send(embed=embeds[0] if len(embeds) == 1 else None,
|
await channel.send(
|
||||||
embeds=embeds if len(embeds) > 1 else None,
|
embed=embeds[0] if len(embeds) == 1 else None,
|
||||||
view=view)
|
embeds=embeds if len(embeds) > 1 else None,
|
||||||
|
view=view,
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _edit_message(self, chat_id: Union[str, int], message_id: Union[str, int],
|
async def _edit_message(
|
||||||
content: Optional[str], embed: Optional[discord.Embed],
|
self,
|
||||||
view: Optional[discord.ui.View], embeds: Optional[List[discord.Embed]] = None) -> bool:
|
chat_id: Union[str, int],
|
||||||
|
message_id: Union[str, int],
|
||||||
|
content: Optional[str],
|
||||||
|
embed: Optional[discord.Embed],
|
||||||
|
view: Optional[discord.ui.View],
|
||||||
|
embeds: Optional[List[discord.Embed]] = None,
|
||||||
|
) -> bool:
|
||||||
channel = await self._resolve_channel(chat_id=str(chat_id))
|
channel = await self._resolve_channel(chat_id=str(chat_id))
|
||||||
if not channel:
|
if not channel:
|
||||||
logger.error(f"未找到要编辑的 Discord 频道:{chat_id}")
|
logger.error(f"未找到要编辑的 Discord 频道:{chat_id}")
|
||||||
@@ -349,7 +447,9 @@ class Discord:
|
|||||||
logger.error(f"编辑 Discord 消息失败:{err}")
|
logger.error(f"编辑 Discord 消息失败:{err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _delete_message(self, message_id: Union[str, int], chat_id: Optional[str]) -> bool:
|
async def _delete_message(
|
||||||
|
self, message_id: Union[str, int], chat_id: Optional[str]
|
||||||
|
) -> bool:
|
||||||
channel = await self._resolve_channel(chat_id=chat_id)
|
channel = await self._resolve_channel(chat_id=chat_id)
|
||||||
if not channel:
|
if not channel:
|
||||||
logger.error("删除 Discord 消息时未找到频道")
|
logger.error("删除 Discord 消息时未找到频道")
|
||||||
@@ -363,11 +463,17 @@ class Discord:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_embed(title: str, text: Optional[str], image: Optional[str],
|
def _build_embed(
|
||||||
link: Optional[str], mtype: Optional['NotificationType'] = None) -> discord.Embed:
|
title: str,
|
||||||
|
text: Optional[str],
|
||||||
|
image: Optional[str],
|
||||||
|
link: Optional[str],
|
||||||
|
mtype: Optional["NotificationType"] = None,
|
||||||
|
) -> discord.Embed:
|
||||||
fields: List[Dict[str, str]] = []
|
fields: List[Dict[str, str]] = []
|
||||||
desc_lines: List[str] = []
|
desc_lines: List[str] = []
|
||||||
should_parse_fields = mtype in PARSE_FIELD_TYPES if mtype else False
|
should_parse_fields = mtype in PARSE_FIELD_TYPES if mtype else False
|
||||||
|
|
||||||
def _collect_spans(s: str, left: str, right: str) -> List[Tuple[int, int]]:
|
def _collect_spans(s: str, left: str, right: str) -> List[Tuple[int, int]]:
|
||||||
spans: List[Tuple[int, int]] = []
|
spans: List[Tuple[int, int]] = []
|
||||||
start = 0
|
start = 0
|
||||||
@@ -383,7 +489,7 @@ class Discord:
|
|||||||
return spans
|
return spans
|
||||||
|
|
||||||
def _find_colon_index(s: str, m: re.Match) -> Optional[int]:
|
def _find_colon_index(s: str, m: re.Match) -> Optional[int]:
|
||||||
segment = s[m.start():m.end()]
|
segment = s[m.start() : m.end()]
|
||||||
for i, ch in enumerate(segment):
|
for i, ch in enumerate(segment):
|
||||||
if ch in (":", ":"):
|
if ch in (":", ":"):
|
||||||
return m.start() + i
|
return m.start() + i
|
||||||
@@ -392,7 +498,11 @@ class Discord:
|
|||||||
if text:
|
if text:
|
||||||
# 处理上游未反序列化的 "\n" 等转义换行,避免被当成普通字符
|
# 处理上游未反序列化的 "\n" 等转义换行,避免被当成普通字符
|
||||||
if "\\n" in text or "\\r" in text:
|
if "\\n" in text or "\\r" in text:
|
||||||
text = text.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\r", "\n")
|
text = (
|
||||||
|
text.replace("\\r\\n", "\n")
|
||||||
|
.replace("\\n", "\n")
|
||||||
|
.replace("\\r", "\n")
|
||||||
|
)
|
||||||
if not should_parse_fields:
|
if not should_parse_fields:
|
||||||
desc_lines.append(text.strip())
|
desc_lines.append(text.strip())
|
||||||
else:
|
else:
|
||||||
@@ -410,12 +520,16 @@ class Discord:
|
|||||||
continue
|
continue
|
||||||
matches = list(pair_pattern.finditer(line))
|
matches = list(pair_pattern.finditer(line))
|
||||||
if matches:
|
if matches:
|
||||||
book_spans = _collect_spans(line, "《", "》") + _collect_spans(line, "【", "】")
|
book_spans = _collect_spans(line, "《", "》") + _collect_spans(
|
||||||
|
line, "【", "】"
|
||||||
|
)
|
||||||
if book_spans:
|
if book_spans:
|
||||||
has_book_colon = False
|
has_book_colon = False
|
||||||
for m in matches:
|
for m in matches:
|
||||||
colon_idx = _find_colon_index(line, m)
|
colon_idx = _find_colon_index(line, m)
|
||||||
if colon_idx is not None and any(l < colon_idx < r for l, r in book_spans):
|
if colon_idx is not None and any(
|
||||||
|
l < colon_idx < r for l, r in book_spans
|
||||||
|
):
|
||||||
has_book_colon = True
|
has_book_colon = True
|
||||||
break
|
break
|
||||||
if has_book_colon:
|
if has_book_colon:
|
||||||
@@ -423,20 +537,25 @@ class Discord:
|
|||||||
continue
|
continue
|
||||||
# 若整行只是 URL/时间等自然包含":"的内容,则不当作字段
|
# 若整行只是 URL/时间等自然包含":"的内容,则不当作字段
|
||||||
url_like_names = {"http", "https", "ftp", "ftps", "magnet"}
|
url_like_names = {"http", "https", "ftp", "ftps", "magnet"}
|
||||||
if all(m.group(1).lower() in url_like_names or m.group(1).isdigit() for m in matches):
|
if all(
|
||||||
|
m.group(1).lower() in url_like_names or m.group(1).isdigit()
|
||||||
|
for m in matches
|
||||||
|
):
|
||||||
desc_lines.append(line)
|
desc_lines.append(line)
|
||||||
continue
|
continue
|
||||||
last_end = 0
|
last_end = 0
|
||||||
for m in matches:
|
for m in matches:
|
||||||
# 追加匹配前的非空文本到描述
|
# 追加匹配前的非空文本到描述
|
||||||
prefix = line[last_end:m.start()].strip(" ,,;;。、")
|
prefix = line[last_end : m.start()].strip(" ,,;;。、")
|
||||||
# 仅当前缀不全是分隔符/空白时才记录
|
# 仅当前缀不全是分隔符/空白时才记录
|
||||||
if prefix and prefix.strip(" ,,;;。、"):
|
if prefix and prefix.strip(" ,,;;。、"):
|
||||||
desc_lines.append(prefix)
|
desc_lines.append(prefix)
|
||||||
name = m.group(1).strip()
|
name = m.group(1).strip()
|
||||||
value = m.group(2).strip(" ,,;;。、\t") or "-"
|
value = m.group(2).strip(" ,,;;。、\t") or "-"
|
||||||
if name:
|
if name:
|
||||||
fields.append({"name": name, "value": value, "inline": False})
|
fields.append(
|
||||||
|
{"name": name, "value": value, "inline": False}
|
||||||
|
)
|
||||||
last_end = m.end()
|
last_end = m.end()
|
||||||
# 匹配末尾后的文本
|
# 匹配末尾后的文本
|
||||||
suffix = line[last_end:].strip(" ,,;;。、")
|
suffix = line[last_end:].strip(" ,,;;。、")
|
||||||
@@ -451,7 +570,7 @@ class Discord:
|
|||||||
title=title,
|
title=title,
|
||||||
url=link or "https://github.com/jxxghp/MoviePilot",
|
url=link or "https://github.com/jxxghp/MoviePilot",
|
||||||
description=description if description else None,
|
description=description if description else None,
|
||||||
color=0xE67E22
|
color=0xE67E22,
|
||||||
)
|
)
|
||||||
for field in fields:
|
for field in fields:
|
||||||
embed.add_field(name=field["name"], value=field["value"], inline=False)
|
embed.add_field(name=field["name"], value=field["value"], inline=False)
|
||||||
@@ -465,14 +584,16 @@ class Discord:
|
|||||||
for index, media in enumerate(medias[:10], start=1):
|
for index, media in enumerate(medias[:10], start=1):
|
||||||
overview = media.get_overview_string(80)
|
overview = media.get_overview_string(80)
|
||||||
desc_parts = [
|
desc_parts = [
|
||||||
f"{media.type.value} | {media.vote_star}" if media.vote_star else media.type.value,
|
f"{media.type.value} | {media.vote_star}"
|
||||||
overview
|
if media.vote_star
|
||||||
|
else media.type.value,
|
||||||
|
overview,
|
||||||
]
|
]
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"{index}. {media.title_year}",
|
title=f"{index}. {media.title_year}",
|
||||||
url=media.detail_link or discord.Embed.Empty,
|
url=media.detail_link or discord.Embed.Empty,
|
||||||
description="\n".join([p for p in desc_parts if p]),
|
description="\n".join([p for p in desc_parts if p]),
|
||||||
color=0x5865F2
|
color=0x5865F2,
|
||||||
)
|
)
|
||||||
if media.get_poster_image():
|
if media.get_poster_image():
|
||||||
embed.set_thumbnail(url=media.get_poster_image())
|
embed.set_thumbnail(url=media.get_poster_image())
|
||||||
@@ -482,7 +603,9 @@ class Discord:
|
|||||||
return embeds
|
return embeds
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_torrent_embeds(torrents: List[Context], title: str) -> List[discord.Embed]:
|
def _build_torrent_embeds(
|
||||||
|
torrents: List[Context], title: str
|
||||||
|
) -> List[discord.Embed]:
|
||||||
embeds: List[discord.Embed] = []
|
embeds: List[discord.Embed] = []
|
||||||
for index, context in enumerate(torrents[:10], start=1):
|
for index, context in enumerate(torrents[:10], start=1):
|
||||||
torrent = context.torrent_info
|
torrent = context.torrent_info
|
||||||
@@ -492,13 +615,13 @@ class Discord:
|
|||||||
detail = [
|
detail = [
|
||||||
f"{torrent.site_name} | {StringUtils.str_filesize(torrent.size)} | {torrent.volume_factor} | {torrent.seeders}↑",
|
f"{torrent.site_name} | {StringUtils.str_filesize(torrent.size)} | {torrent.volume_factor} | {torrent.seeders}↑",
|
||||||
meta.resource_term,
|
meta.resource_term,
|
||||||
meta.video_term
|
meta.video_term,
|
||||||
]
|
]
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"{index}. {title_text or torrent.title}",
|
title=f"{index}. {title_text or torrent.title}",
|
||||||
url=torrent.page_url or discord.Embed.Empty,
|
url=torrent.page_url or discord.Embed.Empty,
|
||||||
description="\n".join([d for d in detail if d]),
|
description="\n".join([d for d in detail if d]),
|
||||||
color=0x00A86B
|
color=0x00A86B,
|
||||||
)
|
)
|
||||||
poster = getattr(torrent, "poster", None)
|
poster = getattr(torrent, "poster", None)
|
||||||
if poster:
|
if poster:
|
||||||
@@ -524,7 +647,9 @@ class Discord:
|
|||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_view(buttons: Optional[List[List[dict]]], link: Optional[str] = None) -> Optional[discord.ui.View]:
|
def _build_view(
|
||||||
|
buttons: Optional[List[List[dict]]], link: Optional[str] = None
|
||||||
|
) -> Optional[discord.ui.View]:
|
||||||
has_buttons = buttons and any(buttons)
|
has_buttons = buttons and any(buttons)
|
||||||
if not has_buttons and not link:
|
if not has_buttons and not link:
|
||||||
return None
|
return None
|
||||||
@@ -534,20 +659,34 @@ class Discord:
|
|||||||
for row_index, button_row in enumerate(buttons[:5]):
|
for row_index, button_row in enumerate(buttons[:5]):
|
||||||
for button in button_row[:5]:
|
for button in button_row[:5]:
|
||||||
if "url" in button:
|
if "url" in button:
|
||||||
btn = discord.ui.Button(label=button.get("text", "链接"),
|
btn = discord.ui.Button(
|
||||||
url=button["url"],
|
label=button.get("text", "链接"),
|
||||||
style=discord.ButtonStyle.link)
|
url=button["url"],
|
||||||
|
style=discord.ButtonStyle.link,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
custom_id = (button.get("callback_data") or button.get("text") or f"btn-{row_index}")[:99]
|
custom_id = (
|
||||||
btn = discord.ui.Button(label=button.get("text", "选择")[:80],
|
button.get("callback_data")
|
||||||
custom_id=custom_id,
|
or button.get("text")
|
||||||
style=discord.ButtonStyle.primary)
|
or f"btn-{row_index}"
|
||||||
|
)[:99]
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=button.get("text", "选择")[:80],
|
||||||
|
custom_id=custom_id,
|
||||||
|
style=discord.ButtonStyle.primary,
|
||||||
|
)
|
||||||
view.add_item(btn)
|
view.add_item(btn)
|
||||||
elif link:
|
elif link:
|
||||||
view.add_item(discord.ui.Button(label="查看详情", url=link, style=discord.ButtonStyle.link))
|
view.add_item(
|
||||||
|
discord.ui.Button(
|
||||||
|
label="查看详情", url=link, style=discord.ButtonStyle.link
|
||||||
|
)
|
||||||
|
)
|
||||||
return view
|
return view
|
||||||
|
|
||||||
async def _resolve_channel(self, userid: Optional[str] = None, chat_id: Optional[str] = None):
|
async def _resolve_channel(
|
||||||
|
self, userid: Optional[str] = None, chat_id: Optional[str] = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Resolve the channel to send messages to.
|
Resolve the channel to send messages to.
|
||||||
Priority order:
|
Priority order:
|
||||||
@@ -557,8 +696,10 @@ class Discord:
|
|||||||
4. Any available text channel in configured guild - fallback
|
4. Any available text channel in configured guild - fallback
|
||||||
5. `userid` (DM) - for private conversations as a final fallback
|
5. `userid` (DM) - for private conversations as a final fallback
|
||||||
"""
|
"""
|
||||||
logger.debug(f"[Discord] _resolve_channel: userid={userid}, chat_id={chat_id}, "
|
logger.debug(
|
||||||
f"_channel_id={self._channel_id}, _guild_id={self._guild_id}")
|
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)
|
# Priority 1: Use explicit chat_id (reply to the same channel where user sent message)
|
||||||
if chat_id:
|
if chat_id:
|
||||||
@@ -585,7 +726,9 @@ class Discord:
|
|||||||
return channel
|
return channel
|
||||||
try:
|
try:
|
||||||
channel = await self._client.fetch_channel(int(mapped_chat_id))
|
channel = await self._client.fetch_channel(int(mapped_chat_id))
|
||||||
logger.debug(f"[Discord] 通过 fetch_channel 找到映射频道: {channel}")
|
logger.debug(
|
||||||
|
f"[Discord] 通过 fetch_channel 找到映射频道: {channel}"
|
||||||
|
)
|
||||||
return channel
|
return channel
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warn(f"通过映射的 chat_id 获取 Discord 频道失败:{err}")
|
logger.warn(f"通过映射的 chat_id 获取 Discord 频道失败:{err}")
|
||||||
@@ -595,7 +738,9 @@ class Discord:
|
|||||||
logger.debug(f"[Discord] 使用缓存的广播频道: {self._broadcast_channel}")
|
logger.debug(f"[Discord] 使用缓存的广播频道: {self._broadcast_channel}")
|
||||||
return self._broadcast_channel
|
return self._broadcast_channel
|
||||||
if self._channel_id:
|
if self._channel_id:
|
||||||
logger.debug(f"[Discord] 尝试通过配置的 _channel_id={self._channel_id} 获取频道")
|
logger.debug(
|
||||||
|
f"[Discord] 尝试通过配置的 _channel_id={self._channel_id} 获取频道"
|
||||||
|
)
|
||||||
channel = self._client.get_channel(self._channel_id)
|
channel = self._client.get_channel(self._channel_id)
|
||||||
if not channel:
|
if not channel:
|
||||||
try:
|
try:
|
||||||
@@ -641,7 +786,9 @@ class Discord:
|
|||||||
async def _get_dm_channel(self, userid: str) -> Optional[discord.DMChannel]:
|
async def _get_dm_channel(self, userid: str) -> Optional[discord.DMChannel]:
|
||||||
logger.debug(f"[Discord] _get_dm_channel: userid={userid}")
|
logger.debug(f"[Discord] _get_dm_channel: userid={userid}")
|
||||||
if userid in self._user_dm_cache:
|
if userid in self._user_dm_cache:
|
||||||
logger.debug(f"[Discord] 从缓存获取私聊频道: {self._user_dm_cache.get(userid)}")
|
logger.debug(
|
||||||
|
f"[Discord] 从缓存获取私聊频道: {self._user_dm_cache.get(userid)}"
|
||||||
|
)
|
||||||
return self._user_dm_cache.get(userid)
|
return self._user_dm_cache.get(userid)
|
||||||
try:
|
try:
|
||||||
logger.debug(f"[Discord] 尝试获取/创建用户 {userid} 的私聊频道")
|
logger.debug(f"[Discord] 尝试获取/创建用户 {userid} 的私聊频道")
|
||||||
@@ -674,7 +821,9 @@ class Discord:
|
|||||||
"""
|
"""
|
||||||
if userid and chat_id:
|
if userid and chat_id:
|
||||||
self._user_chat_mapping[userid] = chat_id
|
self._user_chat_mapping[userid] = chat_id
|
||||||
logger.debug(f"[Discord] 更新用户频道映射: userid={userid} -> chat_id={chat_id}")
|
logger.debug(
|
||||||
|
f"[Discord] 更新用户频道映射: userid={userid} -> chat_id={chat_id}"
|
||||||
|
)
|
||||||
|
|
||||||
def _get_user_chat_id(self, userid: str) -> Optional[str]:
|
def _get_user_chat_id(self, userid: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@@ -708,7 +857,9 @@ class Discord:
|
|||||||
proxy = None
|
proxy = None
|
||||||
if settings.PROXY:
|
if settings.PROXY:
|
||||||
proxy = settings.PROXY.get("https") or settings.PROXY.get("http")
|
proxy = settings.PROXY.get("https") or settings.PROXY.get("http")
|
||||||
async with httpx.AsyncClient(timeout=10, verify=False, proxy=proxy) as client:
|
async with httpx.AsyncClient(
|
||||||
|
timeout=10, verify=False, proxy=proxy
|
||||||
|
) as client:
|
||||||
await client.post(self._ds_url, json=payload)
|
await client.post(self._ds_url, json=payload)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"转发 Discord 消息失败:{err}")
|
logger.error(f"转发 Discord 消息失败:{err}")
|
||||||
|
|||||||
+153
-32
@@ -6,18 +6,16 @@ from app.core.context import MediaInfo, Context
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase, _MessageBase
|
from app.modules import _ModuleBase, _MessageBase
|
||||||
from app.modules.slack.slack import Slack
|
from app.modules.slack.slack import Slack
|
||||||
from app.schemas import MessageChannel, CommingMessage, Notification
|
from app.schemas import MessageChannel, CommingMessage, Notification, MessageResponse
|
||||||
from app.schemas.types import ModuleType
|
from app.schemas.types import ModuleType
|
||||||
|
|
||||||
|
|
||||||
class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
||||||
|
|
||||||
def init_module(self) -> None:
|
def init_module(self) -> None:
|
||||||
"""
|
"""
|
||||||
初始化模块
|
初始化模块
|
||||||
"""
|
"""
|
||||||
super().init_service(service_name=Slack.__name__.lower(),
|
super().init_service(service_name=Slack.__name__.lower(), service_type=Slack)
|
||||||
service_type=Slack)
|
|
||||||
self._channel = MessageChannel.Slack
|
self._channel = MessageChannel.Slack
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -67,7 +65,9 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def message_parser(self, source: str, body: Any, form: Any, args: Any) -> Optional[CommingMessage]:
|
def message_parser(
|
||||||
|
self, source: str, body: Any, form: Any, args: Any
|
||||||
|
) -> Optional[CommingMessage]:
|
||||||
"""
|
"""
|
||||||
解析消息内容,返回字典,注意以下约定值:
|
解析消息内容,返回字典,注意以下约定值:
|
||||||
userid: 用户ID
|
userid: 用户ID
|
||||||
@@ -213,10 +213,14 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
message_info = msg_json.get("message", {})
|
message_info = msg_json.get("message", {})
|
||||||
# Slack消息的时间戳作为消息ID
|
# Slack消息的时间戳作为消息ID
|
||||||
message_ts = message_info.get("ts")
|
message_ts = message_info.get("ts")
|
||||||
channel_id = msg_json.get("channel", {}).get("id") or msg_json.get("container", {}).get("channel_id")
|
channel_id = msg_json.get("channel", {}).get("id") or msg_json.get(
|
||||||
|
"container", {}
|
||||||
|
).get("channel_id")
|
||||||
|
|
||||||
logger.info(f"收到来自 {client_config.name} 的Slack按钮回调:"
|
logger.info(
|
||||||
f"userid={userid}, username={username}, callback_data={callback_data}")
|
f"收到来自 {client_config.name} 的Slack按钮回调:"
|
||||||
|
f"userid={userid}, username={username}, callback_data={callback_data}"
|
||||||
|
)
|
||||||
|
|
||||||
# 创建包含回调信息的CommingMessage
|
# 创建包含回调信息的CommingMessage
|
||||||
return CommingMessage(
|
return CommingMessage(
|
||||||
@@ -228,11 +232,16 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
is_callback=True,
|
is_callback=True,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
message_id=message_ts,
|
message_id=message_ts,
|
||||||
chat_id=channel_id
|
chat_id=channel_id,
|
||||||
)
|
)
|
||||||
elif msg_json.get("type") == "event_callback":
|
elif msg_json.get("type") == "event_callback":
|
||||||
userid = msg_json.get('event', {}).get('user')
|
userid = msg_json.get("event", {}).get("user")
|
||||||
text = re.sub(r"<@[0-9A-Z]+>", "", msg_json.get("event", {}).get("text"), flags=re.IGNORECASE).strip()
|
text = re.sub(
|
||||||
|
r"<@[0-9A-Z]+>",
|
||||||
|
"",
|
||||||
|
msg_json.get("event", {}).get("text"),
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
).strip()
|
||||||
username = ""
|
username = ""
|
||||||
elif msg_json.get("type") == "shortcut":
|
elif msg_json.get("type") == "shortcut":
|
||||||
userid = msg_json.get("user", {}).get("id")
|
userid = msg_json.get("user", {}).get("id")
|
||||||
@@ -244,9 +253,16 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
username = msg_json.get("user_name")
|
username = msg_json.get("user_name")
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
logger.info(f"收到来自 {client_config.name} 的Slack消息:userid={userid}, username={username}, text={text}")
|
logger.info(
|
||||||
return CommingMessage(channel=MessageChannel.Slack, source=client_config.name,
|
f"收到来自 {client_config.name} 的Slack消息:userid={userid}, username={username}, text={text}"
|
||||||
userid=userid, username=username, text=text)
|
)
|
||||||
|
return CommingMessage(
|
||||||
|
channel=MessageChannel.Slack,
|
||||||
|
source=client_config.name,
|
||||||
|
userid=userid,
|
||||||
|
username=username,
|
||||||
|
text=text,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def post_message(self, message: Notification, **kwargs) -> None:
|
def post_message(self, message: Notification, **kwargs) -> None:
|
||||||
@@ -261,19 +277,26 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
targets = message.targets
|
targets = message.targets
|
||||||
userid = message.userid
|
userid = message.userid
|
||||||
if not userid and targets is not None:
|
if not userid and targets is not None:
|
||||||
userid = targets.get('slack_userid')
|
userid = targets.get("slack_userid")
|
||||||
if not userid:
|
if not userid:
|
||||||
logger.warn(f"用户没有指定 Slack用户ID,消息无法发送")
|
logger.warn(f"用户没有指定 Slack用户ID,消息无法发送")
|
||||||
return
|
return
|
||||||
client: Slack = self.get_instance(conf.name)
|
client: Slack = self.get_instance(conf.name)
|
||||||
if client:
|
if client:
|
||||||
client.send_msg(title=message.title, text=message.text,
|
client.send_msg(
|
||||||
image=message.image, userid=userid, link=message.link,
|
title=message.title,
|
||||||
buttons=message.buttons,
|
text=message.text,
|
||||||
original_message_id=message.original_message_id,
|
image=message.image,
|
||||||
original_chat_id=message.original_chat_id)
|
userid=userid,
|
||||||
|
link=message.link,
|
||||||
|
buttons=message.buttons,
|
||||||
|
original_message_id=message.original_message_id,
|
||||||
|
original_chat_id=message.original_chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
|
def post_medias_message(
|
||||||
|
self, message: Notification, medias: List[MediaInfo]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
发送媒体信息选择列表
|
发送媒体信息选择列表
|
||||||
:param message: 消息体
|
:param message: 消息体
|
||||||
@@ -285,12 +308,18 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
continue
|
continue
|
||||||
client: Slack = self.get_instance(conf.name)
|
client: Slack = self.get_instance(conf.name)
|
||||||
if client:
|
if client:
|
||||||
client.send_medias_msg(title=message.title, medias=medias, userid=message.userid,
|
client.send_medias_msg(
|
||||||
buttons=message.buttons,
|
title=message.title,
|
||||||
original_message_id=message.original_message_id,
|
medias=medias,
|
||||||
original_chat_id=message.original_chat_id)
|
userid=message.userid,
|
||||||
|
buttons=message.buttons,
|
||||||
|
original_message_id=message.original_message_id,
|
||||||
|
original_chat_id=message.original_chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None:
|
def post_torrents_message(
|
||||||
|
self, message: Notification, torrents: List[Context]
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
发送种子信息选择列表
|
发送种子信息选择列表
|
||||||
:param message: 消息体
|
:param message: 消息体
|
||||||
@@ -302,13 +331,22 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
continue
|
continue
|
||||||
client: Slack = self.get_instance(conf.name)
|
client: Slack = self.get_instance(conf.name)
|
||||||
if client:
|
if client:
|
||||||
client.send_torrents_msg(title=message.title, torrents=torrents,
|
client.send_torrents_msg(
|
||||||
userid=message.userid, buttons=message.buttons,
|
title=message.title,
|
||||||
original_message_id=message.original_message_id,
|
torrents=torrents,
|
||||||
original_chat_id=message.original_chat_id)
|
userid=message.userid,
|
||||||
|
buttons=message.buttons,
|
||||||
|
original_message_id=message.original_message_id,
|
||||||
|
original_chat_id=message.original_chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
def delete_message(self, channel: MessageChannel, source: str,
|
def delete_message(
|
||||||
message_id: str, chat_id: Optional[str] = None) -> bool:
|
self,
|
||||||
|
channel: MessageChannel,
|
||||||
|
source: str,
|
||||||
|
message_id: str,
|
||||||
|
chat_id: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
删除消息
|
删除消息
|
||||||
:param channel: 消息渠道
|
:param channel: 消息渠道
|
||||||
@@ -329,3 +367,86 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
|||||||
if result:
|
if result:
|
||||||
success = True
|
success = True
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
def edit_message(
|
||||||
|
self,
|
||||||
|
channel: MessageChannel,
|
||||||
|
source: str,
|
||||||
|
message_id: Union[str, int],
|
||||||
|
chat_id: Union[str, int],
|
||||||
|
text: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
编辑消息
|
||||||
|
:param channel: 消息渠道
|
||||||
|
:param source: 指定的消息源
|
||||||
|
:param message_id: 消息ID
|
||||||
|
:param chat_id: 聊天ID
|
||||||
|
:param text: 新的消息内容
|
||||||
|
:param title: 消息标题
|
||||||
|
:return: 编辑是否成功
|
||||||
|
"""
|
||||||
|
if channel != self._channel:
|
||||||
|
return False
|
||||||
|
for conf in self.get_configs().values():
|
||||||
|
if source != conf.name:
|
||||||
|
continue
|
||||||
|
client: Slack = self.get_instance(conf.name)
|
||||||
|
if client:
|
||||||
|
result = client.send_msg(
|
||||||
|
title=title or "",
|
||||||
|
text=text,
|
||||||
|
original_message_id=str(message_id),
|
||||||
|
original_chat_id=str(chat_id),
|
||||||
|
)
|
||||||
|
if result and result[0]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_direct_message(self, message: Notification) -> Optional[MessageResponse]:
|
||||||
|
"""
|
||||||
|
直接发送消息并返回消息ID等信息
|
||||||
|
:param message: 消息体
|
||||||
|
:return: 消息响应(包含message_id, chat_id等)
|
||||||
|
"""
|
||||||
|
for conf in self.get_configs().values():
|
||||||
|
if not self.check_message(message, conf.name):
|
||||||
|
continue
|
||||||
|
targets = message.targets
|
||||||
|
userid = message.userid
|
||||||
|
if not userid and targets is not None:
|
||||||
|
userid = targets.get("slack_userid")
|
||||||
|
if not userid:
|
||||||
|
logger.warn("用户没有指定 Slack 用户ID,消息无法发送")
|
||||||
|
return None
|
||||||
|
client: Slack = self.get_instance(conf.name)
|
||||||
|
if client:
|
||||||
|
result = client.send_msg(
|
||||||
|
title=message.title or "",
|
||||||
|
text=message.text,
|
||||||
|
userid=userid,
|
||||||
|
)
|
||||||
|
if result and result[0]:
|
||||||
|
# Slack 使用时间戳作为 message_id,chat_id 是频道ID
|
||||||
|
# 注意:这里返回的是发送后的结果,需要获取实际的 message_id
|
||||||
|
# 由于 Slack API 返回的是 result[1],包含完整响应,我们需要从中提取
|
||||||
|
response_data = result[1]
|
||||||
|
message_id = (
|
||||||
|
response_data.get("ts")
|
||||||
|
if isinstance(response_data, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
channel_id = (
|
||||||
|
response_data.get("channel")
|
||||||
|
if isinstance(response_data, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return MessageResponse(
|
||||||
|
message_id=message_id,
|
||||||
|
chat_id=channel_id,
|
||||||
|
channel=MessageChannel.Slack,
|
||||||
|
source=conf.name,
|
||||||
|
success=True,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user