mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08:00
feat(feishu): enhance message target resolution and add user ID type handling
This commit is contained in:
@@ -1,7 +1,4 @@
|
|||||||
import asyncio
|
from typing import Any, List, Optional, Tuple, Union
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
from app.core.context import Context, MediaInfo
|
from app.core.context import Context, MediaInfo
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
@@ -54,8 +51,31 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
|||||||
"""通知模块通过系统通知配置控制实例化,这里不额外设置环境开关。"""
|
"""通知模块通过系统通知配置控制实例化,这里不额外设置环境开关。"""
|
||||||
return None
|
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(
|
def message_parser(
|
||||||
self, source: str, body: Any, form: Any, args: Any
|
self, source: str, body: Any, form: Any, args: Any
|
||||||
) -> Optional[CommingMessage]:
|
) -> Optional[CommingMessage]:
|
||||||
client_config = self.get_config(source)
|
client_config = self.get_config(source)
|
||||||
if not client_config:
|
if not client_config:
|
||||||
@@ -69,45 +89,55 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
|||||||
for conf in self.get_configs().values():
|
for conf in self.get_configs().values():
|
||||||
if not self.check_message(message, conf.name):
|
if not self.check_message(message, conf.name):
|
||||||
continue
|
continue
|
||||||
targets = message.targets
|
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||||
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")
|
|
||||||
client: Feishu = self.get_instance(conf.name)
|
client: Feishu = self.get_instance(conf.name)
|
||||||
if client:
|
if client:
|
||||||
client.send_notification(
|
client.send_notification(
|
||||||
message=message,
|
message=message,
|
||||||
userid=str(userid).strip() if userid else None,
|
userid=userid,
|
||||||
chat_id=str(chat_id).strip() if chat_id else None,
|
chat_id=chat_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
|
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
|
||||||
for conf in self.get_configs().values():
|
for conf in self.get_configs().values():
|
||||||
if not self.check_message(message, conf.name):
|
if not self.check_message(message, conf.name):
|
||||||
continue
|
continue
|
||||||
|
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||||
client: Feishu = self.get_instance(conf.name)
|
client: Feishu = self.get_instance(conf.name)
|
||||||
if client:
|
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:
|
def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None:
|
||||||
for conf in self.get_configs().values():
|
for conf in self.get_configs().values():
|
||||||
if not self.check_message(message, conf.name):
|
if not self.check_message(message, conf.name):
|
||||||
continue
|
continue
|
||||||
|
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||||
client: Feishu = self.get_instance(conf.name)
|
client: Feishu = self.get_instance(conf.name)
|
||||||
if client:
|
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(
|
def edit_message(
|
||||||
self,
|
self,
|
||||||
channel: MessageChannel,
|
channel: MessageChannel,
|
||||||
source: str,
|
source: str,
|
||||||
message_id: Union[str, int],
|
message_id: Union[str, int],
|
||||||
chat_id: Union[str, int],
|
chat_id: Union[str, int],
|
||||||
text: str,
|
text: str,
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
buttons: Optional[List[List[dict]]] = None,
|
buttons: Optional[List[List[dict]]] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if channel != self._channel:
|
if channel != self._channel:
|
||||||
return False
|
return False
|
||||||
@@ -116,10 +146,10 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
|||||||
continue
|
continue
|
||||||
client: Feishu = self.get_instance(conf.name)
|
client: Feishu = self.get_instance(conf.name)
|
||||||
if client and client.edit_message(
|
if client and client.edit_message(
|
||||||
message_id=str(message_id),
|
message_id=str(message_id),
|
||||||
title=title,
|
title=title,
|
||||||
text=text,
|
text=text,
|
||||||
buttons=buttons,
|
buttons=buttons,
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -128,19 +158,15 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
|||||||
for conf in self.get_configs().values():
|
for conf in self.get_configs().values():
|
||||||
if not self.check_message(message, conf.name):
|
if not self.check_message(message, conf.name):
|
||||||
continue
|
continue
|
||||||
targets = message.targets
|
userid, chat_id, receive_id_type = self._resolve_message_target(message)
|
||||||
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")
|
|
||||||
client: Feishu = self.get_instance(conf.name)
|
client: Feishu = self.get_instance(conf.name)
|
||||||
if not client:
|
if not client:
|
||||||
continue
|
continue
|
||||||
result = client.send_notification(
|
result = client.send_notification(
|
||||||
message=message,
|
message=message,
|
||||||
userid=str(userid).strip() if userid else None,
|
userid=userid,
|
||||||
chat_id=str(chat_id).strip() if chat_id else None,
|
chat_id=chat_id,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
)
|
)
|
||||||
if result and result.get("success"):
|
if result and result.get("success"):
|
||||||
return MessageResponse(
|
return MessageResponse(
|
||||||
@@ -151,28 +177,3 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
|||||||
success=True,
|
success=True,
|
||||||
)
|
)
|
||||||
return None
|
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")
|
|
||||||
|
|||||||
+112
-22
@@ -7,11 +7,10 @@ import lark_oapi as lark
|
|||||||
from lark_oapi.api.im.v1 import (
|
from lark_oapi.api.im.v1 import (
|
||||||
CreateMessageRequest,
|
CreateMessageRequest,
|
||||||
CreateMessageRequestBody,
|
CreateMessageRequestBody,
|
||||||
|
PatchMessageRequest,
|
||||||
|
PatchMessageRequestBody,
|
||||||
P2ImMessageReceiveV1,
|
P2ImMessageReceiveV1,
|
||||||
UpdateMessageRequest,
|
|
||||||
UpdateMessageRequestBody,
|
|
||||||
)
|
)
|
||||||
from lark_oapi.card.model import Card
|
|
||||||
from lark_oapi.core.const import FEISHU_DOMAIN
|
from lark_oapi.core.const import FEISHU_DOMAIN
|
||||||
from lark_oapi.core.enum import LogLevel
|
from lark_oapi.core.enum import LogLevel
|
||||||
from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
||||||
@@ -58,6 +57,7 @@ class Feishu:
|
|||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self._ws_thread: Optional[threading.Thread] = None
|
self._ws_thread: Optional[threading.Thread] = None
|
||||||
self._user_chat_mapping: Dict[str, str] = {}
|
self._user_chat_mapping: Dict[str, str] = {}
|
||||||
|
self._user_receive_id_type_mapping: Dict[str, str] = {}
|
||||||
self._chat_open_mapping: Dict[str, str] = {}
|
self._chat_open_mapping: Dict[str, str] = {}
|
||||||
|
|
||||||
if not self._app_id or not self._app_secret:
|
if not self._app_id or not self._app_secret:
|
||||||
@@ -131,7 +131,8 @@ class Feishu:
|
|||||||
|
|
||||||
threading.Thread(target=_run, daemon=True).start()
|
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)
|
raw_content = getattr(message, "content", None)
|
||||||
if not raw_content:
|
if not raw_content:
|
||||||
@@ -155,6 +156,19 @@ class Feishu:
|
|||||||
self._user_chat_mapping[normalized_userid] = normalized_chat_id
|
self._user_chat_mapping[normalized_userid] = normalized_chat_id
|
||||||
self._chat_open_mapping[normalized_chat_id] = normalized_userid
|
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:
|
def _on_message(self, data: P2ImMessageReceiveV1) -> None:
|
||||||
"""处理飞书长连接收到的普通消息事件。"""
|
"""处理飞书长连接收到的普通消息事件。"""
|
||||||
event = getattr(data, "event", None)
|
event = getattr(data, "event", None)
|
||||||
@@ -180,6 +194,7 @@ class Feishu:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
userid = open_id or user_id
|
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)
|
self._remember_target(userid=userid, chat_id=chat_id)
|
||||||
logger.info(
|
logger.info(
|
||||||
"收到来自 %s 的飞书消息:userid=%s, chat_id=%s, text=%s",
|
"收到来自 %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")
|
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"))
|
self._remember_target(userid=userid, chat_id=payload.get("chat_id"))
|
||||||
logger.info(
|
logger.info(
|
||||||
"收到来自 %s 的飞书按钮回调:userid=%s, callback_data=%s",
|
"收到来自 %s 的飞书按钮回调:userid=%s, callback_data=%s",
|
||||||
@@ -296,7 +315,12 @@ class Feishu:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if text.startswith("/") and self._admins and str(userid) not in self._admins:
|
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 None
|
||||||
|
|
||||||
return CommingMessage(
|
return CommingMessage(
|
||||||
@@ -309,15 +333,28 @@ class Feishu:
|
|||||||
chat_id=message.get("chat_id"),
|
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_userid = (userid or "").strip() or None
|
||||||
resolved_chat_id = (chat_id 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:
|
if not resolved_userid and not resolved_chat_id:
|
||||||
resolved_userid = self._default_open_id
|
resolved_userid = self._default_open_id
|
||||||
resolved_chat_id = self._default_chat_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:
|
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:
|
if resolved_chat_id:
|
||||||
return resolved_chat_id, "chat_id"
|
return resolved_chat_id, "chat_id"
|
||||||
raise ValueError("未找到可发送的飞书目标")
|
raise ValueError("未找到可发送的飞书目标")
|
||||||
@@ -414,11 +451,26 @@ class Feishu:
|
|||||||
"chat_id": getattr(data, "chat_id", None),
|
"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:
|
try:
|
||||||
receive_id, receive_id_type = self._resolve_target(userid=userid, chat_id=chat_id)
|
receive_id, resolved_receive_id_type = self._resolve_target(
|
||||||
result = self._send_message(receive_id, receive_id_type, "text", {"text": text})
|
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:
|
except Exception as err:
|
||||||
logger.error(f"飞书文本消息发送失败:{err}")
|
logger.error(f"飞书文本消息发送失败:{err}")
|
||||||
return {"success": False}
|
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
|
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
|
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(
|
payload = self._build_card(
|
||||||
title=message.title,
|
title=message.title,
|
||||||
@@ -437,8 +495,17 @@ class Feishu:
|
|||||||
buttons=message.buttons,
|
buttons=message.buttons,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
receive_id, receive_id_type = self._resolve_target(userid=userid, chat_id=chat_id)
|
receive_id, resolved_receive_id_type = self._resolve_target(
|
||||||
result = self._send_message(receive_id, receive_id_type, "interactive", {"card": payload})
|
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:
|
except Exception as err:
|
||||||
logger.error(f"飞书通知发送失败:{err}")
|
logger.error(f"飞书通知发送失败:{err}")
|
||||||
return {"success": False}
|
return {"success": False}
|
||||||
@@ -455,13 +522,12 @@ class Feishu:
|
|||||||
|
|
||||||
card = self._build_card(title=title, text=text, link=None, buttons=buttons)
|
card = self._build_card(title=title, text=text, link=None, buttons=buttons)
|
||||||
try:
|
try:
|
||||||
response = self._api_client.im.v1.message.update(
|
response = self._api_client.im.v1.message.patch(
|
||||||
UpdateMessageRequest.builder()
|
PatchMessageRequest.builder()
|
||||||
.message_id(message_id)
|
.message_id(message_id)
|
||||||
.request_body(
|
.request_body(
|
||||||
UpdateMessageRequestBody.builder()
|
PatchMessageRequestBody.builder()
|
||||||
.msg_type("interactive")
|
.content(json.dumps(card, ensure_ascii=False))
|
||||||
.content(json.dumps({"card": card}, ensure_ascii=False))
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
@@ -478,7 +544,14 @@ class Feishu:
|
|||||||
logger.error(f"飞书消息更新失败:{err}")
|
logger.error(f"飞书消息更新失败:{err}")
|
||||||
return False
|
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 = []
|
lines = []
|
||||||
for index, media in enumerate(medias[:10], start=1):
|
for index, media in enumerate(medias[:10], start=1):
|
||||||
@@ -492,9 +565,21 @@ class Feishu:
|
|||||||
userid=message.userid,
|
userid=message.userid,
|
||||||
targets=message.targets,
|
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 = []
|
lines = []
|
||||||
for index, torrent in enumerate(torrents[:10], start=1):
|
for index, torrent in enumerate(torrents[:10], start=1):
|
||||||
@@ -509,4 +594,9 @@ class Feishu:
|
|||||||
userid=message.userid,
|
userid=message.userid,
|
||||||
targets=message.targets,
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from types import ModuleType, SimpleNamespace
|
||||||
|
from unittest.mock import ANY, MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
sys.modules.setdefault("psutil", ModuleType("psutil"))
|
||||||
|
sys.modules.setdefault("cn2an", ModuleType("cn2an"))
|
||||||
|
sys.modules.setdefault("dateparser", ModuleType("dateparser"))
|
||||||
|
sys.modules.setdefault("zhconv", ModuleType("zhconv"))
|
||||||
|
|
||||||
|
if "Pinyin2Hanzi" not in sys.modules:
|
||||||
|
pinyin_module = ModuleType("Pinyin2Hanzi")
|
||||||
|
setattr(pinyin_module, "is_pinyin", lambda value: False)
|
||||||
|
sys.modules["Pinyin2Hanzi"] = pinyin_module
|
||||||
|
|
||||||
|
from app.modules.feishu import FeishuModule
|
||||||
|
from app.modules.feishu.feishu import Feishu
|
||||||
|
from app.schemas import Notification
|
||||||
|
from app.schemas.types import MessageChannel
|
||||||
|
|
||||||
|
|
||||||
|
class TestFeishu(unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def _build_client(**kwargs) -> Feishu:
|
||||||
|
with patch.object(Feishu, "_build_api_client", return_value=MagicMock()), patch.object(
|
||||||
|
Feishu, "_start_ws_client"
|
||||||
|
):
|
||||||
|
return Feishu(
|
||||||
|
FEISHU_APP_ID="cli_test_app_id",
|
||||||
|
FEISHU_APP_SECRET="cli_test_app_secret",
|
||||||
|
name="feishu-test",
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _success_response(message_id="om_test", chat_id="oc_test"):
|
||||||
|
response = MagicMock()
|
||||||
|
response.success.return_value = True
|
||||||
|
response.data = SimpleNamespace(message_id=message_id, chat_id=chat_id)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_message_api(create_response=None, patch_response=None):
|
||||||
|
message_api = SimpleNamespace(
|
||||||
|
create=MagicMock(return_value=create_response),
|
||||||
|
patch=MagicMock(return_value=patch_response),
|
||||||
|
update=MagicMock(),
|
||||||
|
)
|
||||||
|
api_client = SimpleNamespace(im=SimpleNamespace(v1=SimpleNamespace(message=message_api)))
|
||||||
|
return api_client, message_api
|
||||||
|
|
||||||
|
def test_parse_message_returns_callback_message(self):
|
||||||
|
client = self._build_client()
|
||||||
|
|
||||||
|
result = client.parse_message(
|
||||||
|
{
|
||||||
|
"type": "cardAction",
|
||||||
|
"callback_data": "approve",
|
||||||
|
"message_id": "om_123",
|
||||||
|
"chat_id": "oc_123",
|
||||||
|
"sender": {
|
||||||
|
"open_id": "ou_user_1",
|
||||||
|
"user_id": "u_user_1",
|
||||||
|
"name": "tester",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertEqual(result.channel, MessageChannel.Feishu)
|
||||||
|
self.assertEqual(result.userid, "ou_user_1")
|
||||||
|
self.assertEqual(result.text, "CALLBACK:approve")
|
||||||
|
self.assertTrue(result.is_callback)
|
||||||
|
self.assertEqual(result.chat_id, "oc_123")
|
||||||
|
|
||||||
|
def test_parse_message_blocks_non_admin_command(self):
|
||||||
|
client = self._build_client(FEISHU_ADMINS="ou_admin")
|
||||||
|
|
||||||
|
with patch.object(client, "send_text", return_value={"success": True}) as send_text:
|
||||||
|
result = client.parse_message(
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"text": "/help",
|
||||||
|
"chat_id": "oc_chat_1",
|
||||||
|
"sender": {
|
||||||
|
"open_id": "ou_user_2",
|
||||||
|
"user_id": "u_user_2",
|
||||||
|
"name": "tester",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
send_text.assert_called_once_with(
|
||||||
|
"只有管理员才有权限执行此命令",
|
||||||
|
userid="ou_user_2",
|
||||||
|
chat_id="oc_chat_1",
|
||||||
|
receive_id_type="open_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_send_notification_uses_direct_card_content(self):
|
||||||
|
client = self._build_client()
|
||||||
|
client._api_client, message_api = self._build_message_api(
|
||||||
|
create_response=self._success_response()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = client.send_notification(
|
||||||
|
Notification(
|
||||||
|
title="测试标题",
|
||||||
|
text="测试正文",
|
||||||
|
buttons=[[{"text": "确认", "callback_data": "confirm"}]],
|
||||||
|
),
|
||||||
|
userid="ou_user_3",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
request = message_api.create.call_args.args[0]
|
||||||
|
self.assertEqual(request.receive_id_type, "open_id")
|
||||||
|
self.assertEqual(request.request_body.msg_type, "interactive")
|
||||||
|
|
||||||
|
content = json.loads(request.request_body.content)
|
||||||
|
self.assertNotIn("card", content)
|
||||||
|
self.assertEqual(content["elements"][0]["tag"], "markdown")
|
||||||
|
|
||||||
|
def test_send_notification_supports_user_id_target(self):
|
||||||
|
client = self._build_client()
|
||||||
|
client._api_client, message_api = self._build_message_api(
|
||||||
|
create_response=self._success_response()
|
||||||
|
)
|
||||||
|
|
||||||
|
client.send_notification(
|
||||||
|
Notification(title="测试标题", text="测试正文"),
|
||||||
|
userid="u_user_4",
|
||||||
|
receive_id_type="user_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
request = message_api.create.call_args.args[0]
|
||||||
|
self.assertEqual(request.receive_id_type, "user_id")
|
||||||
|
|
||||||
|
def test_edit_message_uses_patch_api_for_cards(self):
|
||||||
|
client = self._build_client()
|
||||||
|
client._api_client, message_api = self._build_message_api(
|
||||||
|
patch_response=self._success_response()
|
||||||
|
)
|
||||||
|
|
||||||
|
success = client.edit_message(
|
||||||
|
message_id="om_456",
|
||||||
|
title="测试标题",
|
||||||
|
text="测试正文",
|
||||||
|
buttons=[[{"text": "确认", "callback_data": "confirm"}]],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(success)
|
||||||
|
message_api.patch.assert_called_once()
|
||||||
|
message_api.update.assert_not_called()
|
||||||
|
|
||||||
|
request = message_api.patch.call_args.args[0]
|
||||||
|
self.assertEqual(request.message_id, "om_456")
|
||||||
|
content = json.loads(request.request_body.content)
|
||||||
|
self.assertNotIn("card", content)
|
||||||
|
self.assertEqual(content["elements"][0]["tag"], "markdown")
|
||||||
|
|
||||||
|
def test_module_send_direct_message_prefers_open_id_target(self):
|
||||||
|
module = FeishuModule()
|
||||||
|
module._channel = MessageChannel.Feishu
|
||||||
|
conf = SimpleNamespace(name="feishu-main")
|
||||||
|
client = MagicMock()
|
||||||
|
client.send_notification.return_value = {
|
||||||
|
"success": True,
|
||||||
|
"message_id": "om_789",
|
||||||
|
"chat_id": "oc_789",
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(module, "get_configs", return_value={"feishu-main": conf}), patch.object(
|
||||||
|
module, "check_message", return_value=True
|
||||||
|
), patch.object(module, "get_instance", return_value=client):
|
||||||
|
response = module.send_direct_message(
|
||||||
|
Notification(
|
||||||
|
targets={
|
||||||
|
"feishu_userid": "u_target",
|
||||||
|
"feishu_openid": "ou_target",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
client.send_notification.assert_called_once_with(
|
||||||
|
message=ANY,
|
||||||
|
userid="ou_target",
|
||||||
|
chat_id=None,
|
||||||
|
receive_id_type="open_id",
|
||||||
|
)
|
||||||
|
self.assertTrue(response.success)
|
||||||
|
self.assertEqual(response.message_id, "om_789")
|
||||||
|
self.assertEqual(response.chat_id, "oc_789")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user