Files
DanDingNoneBot/danding_bot/plugins/danding_qqpush/api.py

173 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""API 接口模块 - FastAPI 路由定义"""
from fastapi import APIRouter, Request, HTTPException
from pydantic import BaseModel
import asyncio
from typing import Optional
from nonebot import logger
from .config import Config
from .article_render import render_article_to_base64
from .text_parser import TextParser
from .image_render import ImageRenderer
from .sender import sender
from .utils import validate_token
# Module-level singleton: load font once, reuse across requests
_renderer: Optional['ImageRenderer'] = None
def _get_renderer(config: Config) -> 'ImageRenderer':
global _renderer
if _renderer is None:
_renderer = ImageRenderer(
width=config.ImageWidth,
font_size=config.ImageFontSize,
padding=config.ImagePadding,
line_spacing=config.ImageLineSpacing,
bg_color=config.ImageBgColor,
text_color=config.ImageTextColor,
font_paths=config.FontPaths,
)
return _renderer
# 请求体模型
class PushRequest(BaseModel):
"""推送请求模型"""
group_id: int
"""接收消息的 QQ 群号"""
qq: Optional[int] = None
"""可选的被 @ QQ 号;未传时仅发送群消息"""
text: str
"""通知文本(# 表示换行)"""
html: Optional[str] = None
"""可选的完整公告 HTML传入时使用虚拟浏览器截图"""
title: Optional[str] = None
"""公告标题"""
published_at: Optional[str] = None
"""公告发布时间"""
# 响应模型
class PushResponse(BaseModel):
"""推送响应模型"""
success: bool
"""是否成功"""
message: str
"""响应消息"""
data: Optional[dict] = None
"""返回数据(如有)"""
# 创建路由器
router = APIRouter()
def create_routes(token: str, config: Config):
"""
创建 API 路由
Args:
token: 鉴权 Token
config: 配置对象
"""
@router.post("/danding/qqpush/{request_token}", response_model=PushResponse)
async def qqpush(request_token: str, request: Request, data: PushRequest):
"""
QQ 消息推送接口
Args:
request: FastAPI 请求对象
data: 推送请求数据
Returns:
推送结果
"""
try:
if not validate_token(request_token, token):
raise HTTPException(status_code=403, detail="Token 验证失败")
# 1. 验证参数
if not data.group_id:
raise HTTPException(status_code=400, detail="group_id 不能为空")
if not data.text or not isinstance(data.text, str):
raise HTTPException(status_code=400, detail="text 不能为空且必须是字符串")
# 2. 检查 Bot 是否在线
bot = sender.get_bot()
if not bot:
logger.error("Bot 实例未设置,无法发送消息")
raise HTTPException(
status_code=500,
detail="Bot 未连接,请检查机器人状态"
)
# 3. 文本处理
text_parser = TextParser(max_length=config.MaxTextLength)
if not text_parser.validate_text(data.text):
raise HTTPException(status_code=400, detail="文本内容无效")
parsed_text = text_parser.parse(data.text)
logger.info(f"解析文本: {parsed_text[:50]}..." if len(parsed_text) > 50 else parsed_text)
# 4. 原始公告使用 Chromium 截图,普通通知保持既有 Pillow 渲染。
if data.html:
image_base64 = await render_article_to_base64(
data.html,
data.title or "蛋定助手通知",
data.published_at,
)
send_result = await sender.send_to_group(
group_id=data.group_id,
image_base64=image_base64,
qq=data.qq,
)
else:
image_base64 = await asyncio.to_thread(_get_renderer(config).render_to_base64, parsed_text)
send_result = await sender.send_to_group(
group_id=data.group_id,
image_base64=image_base64,
qq=data.qq,
)
logger.info("图片生成成功")
if not send_result["success"]:
logger.error(f"消息发送失败: {send_result['error']}")
raise HTTPException(
status_code=500,
detail=send_result["message"]
)
logger.info(f"消息发送成功 - 群: {data.group_id}, @: {data.qq}")
return PushResponse(
success=True,
message="推送成功",
data={
"group_id": data.group_id,
"qq": data.qq,
"message_id": send_result["data"].get("message_id")
}
)
except HTTPException:
raise
except Exception as e:
logger.exception(f"推送接口异常: {e}")
raise HTTPException(
status_code=500,
detail="服务器内部错误"
)
return router