mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-13 07:26:45 +00:00
feat(feishu): enhance message target resolution and add user ID type handling
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
|
||||
from app.core.context import Context, MediaInfo
|
||||
from app.log import logger
|
||||
@@ -54,8 +51,31 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
||||
"""通知模块通过系统通知配置控制实例化,这里不额外设置环境开关。"""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _resolve_message_target(
|
||||
message: Notification,
|
||||
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
"""优先使用 open_id,其次回退 user_id 或 chat_id。"""
|
||||
userid = str(message.userid).strip() if message.userid else None
|
||||
chat_id = None
|
||||
receive_id_type = "open_id" if userid else None
|
||||
|
||||
targets = message.targets or {}
|
||||
if not userid and targets:
|
||||
open_id = str(targets.get("feishu_openid") or "").strip() or None
|
||||
user_id = str(targets.get("feishu_userid") or "").strip() or None
|
||||
chat_id = str(targets.get("feishu_chat_id") or "").strip() or None
|
||||
if open_id:
|
||||
userid = open_id
|
||||
receive_id_type = "open_id"
|
||||
elif user_id:
|
||||
userid = user_id
|
||||
receive_id_type = "user_id"
|
||||
|
||||
return userid, chat_id, receive_id_type
|
||||
|
||||
def message_parser(
|
||||
self, source: str, body: Any, form: Any, args: Any
|
||||
self, source: str, body: Any, form: Any, args: Any
|
||||
) -> Optional[CommingMessage]:
|
||||
client_config = self.get_config(source)
|
||||
if not client_config:
|
||||
@@ -69,45 +89,55 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
||||
for conf in self.get_configs().values():
|
||||
if not self.check_message(message, conf.name):
|
||||
continue
|
||||
targets = message.targets
|
||||
userid = message.userid
|
||||
chat_id = None
|
||||
if not userid and targets is not None:
|
||||
userid = targets.get("feishu_userid") or targets.get("feishu_openid")
|
||||
chat_id = targets.get("feishu_chat_id")
|
||||
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||
client: Feishu = self.get_instance(conf.name)
|
||||
if client:
|
||||
client.send_notification(
|
||||
message=message,
|
||||
userid=str(userid).strip() if userid else None,
|
||||
chat_id=str(chat_id).strip() if chat_id else None,
|
||||
userid=userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
|
||||
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
|
||||
for conf in self.get_configs().values():
|
||||
if not self.check_message(message, conf.name):
|
||||
continue
|
||||
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||
client: Feishu = self.get_instance(conf.name)
|
||||
if client:
|
||||
client.send_medias_message(message=message, medias=medias)
|
||||
client.send_medias_message(
|
||||
message=message,
|
||||
medias=medias,
|
||||
userid=userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
|
||||
def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None:
|
||||
for conf in self.get_configs().values():
|
||||
if not self.check_message(message, conf.name):
|
||||
continue
|
||||
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||
client: Feishu = self.get_instance(conf.name)
|
||||
if client:
|
||||
client.send_torrents_message(message=message, torrents=torrents)
|
||||
client.send_torrents_message(
|
||||
message=message,
|
||||
torrents=torrents,
|
||||
userid=userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
|
||||
def edit_message(
|
||||
self,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
message_id: Union[str, int],
|
||||
chat_id: Union[str, int],
|
||||
text: str,
|
||||
title: Optional[str] = None,
|
||||
buttons: Optional[List[List[dict]]] = None,
|
||||
self,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
message_id: Union[str, int],
|
||||
chat_id: Union[str, int],
|
||||
text: str,
|
||||
title: Optional[str] = None,
|
||||
buttons: Optional[List[List[dict]]] = None,
|
||||
) -> bool:
|
||||
if channel != self._channel:
|
||||
return False
|
||||
@@ -116,10 +146,10 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
||||
continue
|
||||
client: Feishu = self.get_instance(conf.name)
|
||||
if client and client.edit_message(
|
||||
message_id=str(message_id),
|
||||
title=title,
|
||||
text=text,
|
||||
buttons=buttons,
|
||||
message_id=str(message_id),
|
||||
title=title,
|
||||
text=text,
|
||||
buttons=buttons,
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -128,19 +158,15 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
||||
for conf in self.get_configs().values():
|
||||
if not self.check_message(message, conf.name):
|
||||
continue
|
||||
targets = message.targets
|
||||
userid = message.userid
|
||||
chat_id = None
|
||||
if not userid and targets is not None:
|
||||
userid = targets.get("feishu_userid") or targets.get("feishu_openid")
|
||||
chat_id = targets.get("feishu_chat_id")
|
||||
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||
client: Feishu = self.get_instance(conf.name)
|
||||
if not client:
|
||||
continue
|
||||
result = client.send_notification(
|
||||
message=message,
|
||||
userid=str(userid).strip() if userid else None,
|
||||
chat_id=str(chat_id).strip() if chat_id else None,
|
||||
userid=userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
if result and result.get("success"):
|
||||
return MessageResponse(
|
||||
@@ -151,28 +177,3 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
||||
success=True,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def run_async(coro):
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return asyncio.run(coro)
|
||||
|
||||
result: Dict[str, Any] = {}
|
||||
error: Dict[str, BaseException] = {}
|
||||
done = threading.Event()
|
||||
|
||||
def runner():
|
||||
try:
|
||||
result["value"] = asyncio.run(coro)
|
||||
except BaseException as err:
|
||||
error["value"] = err
|
||||
finally:
|
||||
done.set()
|
||||
|
||||
threading.Thread(target=runner, daemon=True).start()
|
||||
done.wait()
|
||||
if error.get("value"):
|
||||
raise error["value"]
|
||||
return result.get("value")
|
||||
|
||||
@@ -7,11 +7,10 @@ import lark_oapi as lark
|
||||
from lark_oapi.api.im.v1 import (
|
||||
CreateMessageRequest,
|
||||
CreateMessageRequestBody,
|
||||
PatchMessageRequest,
|
||||
PatchMessageRequestBody,
|
||||
P2ImMessageReceiveV1,
|
||||
UpdateMessageRequest,
|
||||
UpdateMessageRequestBody,
|
||||
)
|
||||
from lark_oapi.card.model import Card
|
||||
from lark_oapi.core.const import FEISHU_DOMAIN
|
||||
from lark_oapi.core.enum import LogLevel
|
||||
from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
||||
@@ -58,6 +57,7 @@ class Feishu:
|
||||
self._stop_event = threading.Event()
|
||||
self._ws_thread: Optional[threading.Thread] = None
|
||||
self._user_chat_mapping: Dict[str, str] = {}
|
||||
self._user_receive_id_type_mapping: Dict[str, str] = {}
|
||||
self._chat_open_mapping: Dict[str, str] = {}
|
||||
|
||||
if not self._app_id or not self._app_secret:
|
||||
@@ -131,7 +131,8 @@ class Feishu:
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _extract_message_text(self, message) -> str:
|
||||
@staticmethod
|
||||
def _extract_message_text(message) -> str:
|
||||
"""从飞书事件消息体中提取可读文本。"""
|
||||
raw_content = getattr(message, "content", None)
|
||||
if not raw_content:
|
||||
@@ -155,6 +156,19 @@ class Feishu:
|
||||
self._user_chat_mapping[normalized_userid] = normalized_chat_id
|
||||
self._chat_open_mapping[normalized_chat_id] = normalized_userid
|
||||
|
||||
def _remember_user_id_type(
|
||||
self,
|
||||
open_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""记住用户对应的飞书 ID 类型,避免回消息时误用 open_id/user_id。"""
|
||||
normalized_open_id = (open_id or "").strip()
|
||||
normalized_user_id = (user_id or "").strip()
|
||||
if normalized_open_id:
|
||||
self._user_receive_id_type_mapping[normalized_open_id] = "open_id"
|
||||
if normalized_user_id:
|
||||
self._user_receive_id_type_mapping[normalized_user_id] = "user_id"
|
||||
|
||||
def _on_message(self, data: P2ImMessageReceiveV1) -> None:
|
||||
"""处理飞书长连接收到的普通消息事件。"""
|
||||
event = getattr(data, "event", None)
|
||||
@@ -180,6 +194,7 @@ class Feishu:
|
||||
},
|
||||
}
|
||||
userid = open_id or user_id
|
||||
self._remember_user_id_type(open_id=open_id, user_id=user_id)
|
||||
self._remember_target(userid=userid, chat_id=chat_id)
|
||||
logger.info(
|
||||
"收到来自 %s 的飞书消息:userid=%s, chat_id=%s, text=%s",
|
||||
@@ -216,6 +231,10 @@ class Feishu:
|
||||
},
|
||||
}
|
||||
userid = payload["sender"].get("open_id") or payload["sender"].get("user_id")
|
||||
self._remember_user_id_type(
|
||||
open_id=payload["sender"].get("open_id"),
|
||||
user_id=payload["sender"].get("user_id"),
|
||||
)
|
||||
self._remember_target(userid=userid, chat_id=payload.get("chat_id"))
|
||||
logger.info(
|
||||
"收到来自 %s 的飞书按钮回调:userid=%s, callback_data=%s",
|
||||
@@ -296,7 +315,12 @@ class Feishu:
|
||||
return None
|
||||
|
||||
if text.startswith("/") and self._admins and str(userid) not in self._admins:
|
||||
self.send_text("只有管理员才有权限执行此命令", userid=str(userid))
|
||||
self.send_text(
|
||||
"只有管理员才有权限执行此命令",
|
||||
userid=str(userid),
|
||||
chat_id=message.get("chat_id"),
|
||||
receive_id_type="open_id" if open_id else "user_id",
|
||||
)
|
||||
return None
|
||||
|
||||
return CommingMessage(
|
||||
@@ -309,15 +333,28 @@ class Feishu:
|
||||
chat_id=message.get("chat_id"),
|
||||
)
|
||||
|
||||
def _resolve_target(self, userid: Optional[str] = None, chat_id: Optional[str] = None) -> Tuple[str, str]:
|
||||
def _resolve_target(
|
||||
self,
|
||||
userid: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""解析飞书发送目标,优先走显式用户,其次回退默认配置。"""
|
||||
resolved_userid = (userid or "").strip() or None
|
||||
resolved_chat_id = (chat_id or "").strip() or None
|
||||
normalized_receive_id_type = (receive_id_type or "").strip() or None
|
||||
if not resolved_userid and not resolved_chat_id:
|
||||
resolved_userid = self._default_open_id
|
||||
resolved_chat_id = self._default_chat_id
|
||||
if resolved_userid and not normalized_receive_id_type:
|
||||
normalized_receive_id_type = "open_id"
|
||||
if normalized_receive_id_type == "chat_id" and resolved_chat_id:
|
||||
return resolved_chat_id, "chat_id"
|
||||
if resolved_userid:
|
||||
return resolved_userid, "open_id"
|
||||
if normalized_receive_id_type in {"open_id", "user_id"}:
|
||||
return resolved_userid, normalized_receive_id_type
|
||||
remembered_type = self._user_receive_id_type_mapping.get(resolved_userid)
|
||||
return resolved_userid, remembered_type or "open_id"
|
||||
if resolved_chat_id:
|
||||
return resolved_chat_id, "chat_id"
|
||||
raise ValueError("未找到可发送的飞书目标")
|
||||
@@ -414,11 +451,26 @@ class Feishu:
|
||||
"chat_id": getattr(data, "chat_id", None),
|
||||
}
|
||||
|
||||
def send_text(self, text: str, userid: Optional[str] = None, chat_id: Optional[str] = None) -> Optional[dict]:
|
||||
def send_text(
|
||||
self,
|
||||
text: str,
|
||||
userid: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
"""发送纯文本消息。"""
|
||||
try:
|
||||
receive_id, receive_id_type = self._resolve_target(userid=userid, chat_id=chat_id)
|
||||
result = self._send_message(receive_id, receive_id_type, "text", {"text": text})
|
||||
receive_id, resolved_receive_id_type = self._resolve_target(
|
||||
userid=userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
result = self._send_message(
|
||||
receive_id,
|
||||
resolved_receive_id_type,
|
||||
"text",
|
||||
{"text": text},
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(f"飞书文本消息发送失败:{err}")
|
||||
return {"success": False}
|
||||
@@ -428,7 +480,13 @@ class Feishu:
|
||||
result["chat_id"] = result.get("chat_id") or chat_id or self._user_chat_mapping.get(userid or "") or self._default_chat_id
|
||||
return result
|
||||
|
||||
def send_notification(self, message: Notification, userid: Optional[str] = None, chat_id: Optional[str] = None) -> Optional[dict]:
|
||||
def send_notification(
|
||||
self,
|
||||
message: Notification,
|
||||
userid: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
"""发送通知消息,优先使用交互卡片承载按钮。"""
|
||||
payload = self._build_card(
|
||||
title=message.title,
|
||||
@@ -437,8 +495,17 @@ class Feishu:
|
||||
buttons=message.buttons,
|
||||
)
|
||||
try:
|
||||
receive_id, receive_id_type = self._resolve_target(userid=userid, chat_id=chat_id)
|
||||
result = self._send_message(receive_id, receive_id_type, "interactive", {"card": payload})
|
||||
receive_id, resolved_receive_id_type = self._resolve_target(
|
||||
userid=userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
result = self._send_message(
|
||||
receive_id,
|
||||
resolved_receive_id_type,
|
||||
"interactive",
|
||||
payload,
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(f"飞书通知发送失败:{err}")
|
||||
return {"success": False}
|
||||
@@ -455,13 +522,12 @@ class Feishu:
|
||||
|
||||
card = self._build_card(title=title, text=text, link=None, buttons=buttons)
|
||||
try:
|
||||
response = self._api_client.im.v1.message.update(
|
||||
UpdateMessageRequest.builder()
|
||||
response = self._api_client.im.v1.message.patch(
|
||||
PatchMessageRequest.builder()
|
||||
.message_id(message_id)
|
||||
.request_body(
|
||||
UpdateMessageRequestBody.builder()
|
||||
.msg_type("interactive")
|
||||
.content(json.dumps({"card": card}, ensure_ascii=False))
|
||||
PatchMessageRequestBody.builder()
|
||||
.content(json.dumps(card, ensure_ascii=False))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
@@ -478,7 +544,14 @@ class Feishu:
|
||||
logger.error(f"飞书消息更新失败:{err}")
|
||||
return False
|
||||
|
||||
def send_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[dict]:
|
||||
def send_medias_message(
|
||||
self,
|
||||
message: Notification,
|
||||
medias: List[MediaInfo],
|
||||
userid: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
"""发送媒体列表消息,复用通知发送链路。"""
|
||||
lines = []
|
||||
for index, media in enumerate(medias[:10], start=1):
|
||||
@@ -492,9 +565,21 @@ class Feishu:
|
||||
userid=message.userid,
|
||||
targets=message.targets,
|
||||
)
|
||||
return self.send_notification(proxy_message, userid=message.userid)
|
||||
return self.send_notification(
|
||||
proxy_message,
|
||||
userid=userid or message.userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
|
||||
def send_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[dict]:
|
||||
def send_torrents_message(
|
||||
self,
|
||||
message: Notification,
|
||||
torrents: List[Context],
|
||||
userid: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
"""发送种子列表消息,复用通知发送链路。"""
|
||||
lines = []
|
||||
for index, torrent in enumerate(torrents[:10], start=1):
|
||||
@@ -509,4 +594,9 @@ class Feishu:
|
||||
userid=message.userid,
|
||||
targets=message.targets,
|
||||
)
|
||||
return self.send_notification(proxy_message, userid=message.userid)
|
||||
return self.send_notification(
|
||||
proxy_message,
|
||||
userid=userid or message.userid,
|
||||
chat_id=chat_id,
|
||||
receive_id_type=receive_id_type,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user