mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
严格检查全部宿主源码,清零类型错误预算并移除按模块类型豁免。 按原因、所属方法、错误码及语法指纹锁定必要兼容例外和站点类型桩, CI 阻断新增忽略、检查范围退化、本地桩替代真实接口及新的类型错误。 修复空值、异步和供应方类型边界,保持插件与 API 兼容合同, 并补充存储、订阅、通知、Agent 和工作流边界的回归覆盖。
140 lines
5.2 KiB
Python
140 lines
5.2 KiB
Python
"""
|
||
按通知配置向已登记浏览器订阅投递 Web Push 消息。
|
||
"""
|
||
|
||
import json
|
||
from typing import Any, Dict, Optional, Tuple, Union
|
||
|
||
from app.adapters.network.webpush import WebPushDeliveryError, send_webpush
|
||
from app.modules import _MessageBase, _ModuleBase
|
||
from app.runtime.log import logger
|
||
from app.runtime.settings import get_runtime_setting
|
||
from app.runtime.webpush import webpush_registry
|
||
from app.schemas.message import Message
|
||
from app.schemas.types import ModuleType, NotificationChannel
|
||
|
||
|
||
class WebPushModule(_ModuleBase, _MessageBase[object]):
|
||
"""
|
||
通过浏览器 Web Push 通道发送通知消息。
|
||
"""
|
||
|
||
def init_module(self) -> None:
|
||
"""
|
||
读取 Web Push 通知配置并声明浏览器推送渠道。
|
||
"""
|
||
super().init_service(service_name=self.get_name().lower())
|
||
self._channel = NotificationChannel.WebPush
|
||
|
||
@staticmethod
|
||
def get_name() -> str:
|
||
"""
|
||
返回浏览器推送模块的展示名称。
|
||
"""
|
||
return "WebPush"
|
||
|
||
@staticmethod
|
||
def get_type() -> ModuleType:
|
||
"""
|
||
获取模块类型。
|
||
"""
|
||
return ModuleType.Notification
|
||
|
||
@staticmethod
|
||
def get_subtype() -> NotificationChannel:
|
||
"""
|
||
获取模块子类型。
|
||
"""
|
||
return NotificationChannel.WebPush
|
||
|
||
@staticmethod
|
||
def get_priority() -> int:
|
||
"""
|
||
获取模块优先级,数字越小优先级越高,只有同一接口下优先级才生效。
|
||
"""
|
||
return 6
|
||
|
||
def stop(self) -> None:
|
||
"""
|
||
Web Push 模块没有需要释放的长连接资源。
|
||
"""
|
||
pass
|
||
|
||
def test(self) -> Tuple[bool, str]:
|
||
"""
|
||
返回模块可用状态,不向浏览器推送端点发起探测。
|
||
|
||
:return: 固定成功结果;实际投递错误由发送时记录
|
||
"""
|
||
return True, ""
|
||
|
||
def init_setting(self) -> Optional[Tuple[str, Union[str, bool]]]:
|
||
"""
|
||
Web Push 使用全局 VAPID 配置,不提供模块级设置。
|
||
|
||
:return: 始终为 None,表示不检查模块级配置项
|
||
"""
|
||
pass
|
||
|
||
def post_message(self, message: Message, **kwargs: object) -> None:
|
||
"""
|
||
向匹配通知配置的浏览器订阅逐个投递消息。
|
||
|
||
投递失败仅记录日志,不中断其他订阅;端点返回 404 或 410 时移除失效订阅。
|
||
|
||
:param message: 待发送的标题、正文、目标用户与跳转链接
|
||
:param kwargs: 为模块分发接口保留的扩展参数,本实现不使用
|
||
"""
|
||
for conf in self.get_configs().values():
|
||
if not self.check_message(message, conf.name):
|
||
continue
|
||
webpush_users = (conf.config or {}).get("WEBPUSH_USERNAME") or ""
|
||
if webpush_users:
|
||
# 设定了接收用户时,非该用户的消息不接收
|
||
if not message.username or message.username not in webpush_users.split(","):
|
||
continue
|
||
if not message.title and not message.text:
|
||
logger.warn("标题和内容不能同时为空")
|
||
return
|
||
try:
|
||
if message.title:
|
||
caption: Optional[str] = message.title
|
||
content = message.text
|
||
else:
|
||
caption = message.text
|
||
content = ""
|
||
for sub in webpush_registry.list():
|
||
logger.debug(f"给 {sub} 发送WebPush:{caption} {content}")
|
||
try:
|
||
endpoint = sub.get("endpoint")
|
||
webpush_options: Dict[str, Any] = {}
|
||
if endpoint and "notify.windows.com" in endpoint:
|
||
webpush_options = {
|
||
"ttl": 86400,
|
||
"headers": {"X-WNS-Cache-Policy": "cache"},
|
||
}
|
||
send_webpush(
|
||
subscription=sub,
|
||
data=json.dumps({
|
||
"title": caption,
|
||
"body": content,
|
||
"url": message.link or "/?shotcut=message"
|
||
}),
|
||
vapid_private_key=get_runtime_setting('VAPID').get("privateKey"),
|
||
vapid_subject=get_runtime_setting('VAPID').get("subject"),
|
||
**webpush_options,
|
||
)
|
||
except WebPushDeliveryError as err:
|
||
logger.error(f"WebPush发送失败: {str(err)}")
|
||
response = getattr(err, "response", None)
|
||
status_code = getattr(response, "status_code", None) or getattr(
|
||
response,
|
||
"status",
|
||
None,
|
||
)
|
||
if status_code in {404, 410} and webpush_registry.remove(sub):
|
||
logger.info(f"已移除失效WebPush订阅: {sub.get('endpoint')}")
|
||
|
||
except Exception as msg_e:
|
||
logger.error(f"发送消息失败:{msg_e}")
|