feat(cherry-tomato): 新增图片混淆/解混淆插件(小番茄)
Gilbert 曲线像素重排,可与网页版互解;`混淆` / `解混淆` 命令 (回复图片或图文同条消息发送),附 tests/test_cherry_tomato.py Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
968b3d1cfe
commit
fb15b976aa
9 files changed
+862
No files matched your search
@@ -0,0 +1,3 @@
|
||||
"""小番茄图片混淆 —— Trigger 层。"""
|
||||
|
||||
from .obfuscate import deobf_cmd, obf_cmd # noqa: F401
|
||||
@@ -0,0 +1,122 @@
|
||||
"""混淆/解混淆 —— 触发器(Trigger)层。
|
||||
|
||||
回复一张图片后发「混淆」或「解混淆」触发(图片与命令同一条消息直接发也行,
|
||||
不需要 @bot);下载图片 → 变换 → 结果图发回。算法层见 utils/obfuscation.py。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent
|
||||
from nonebot.log import logger
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
from hexi.core.cooldown import cooldown
|
||||
from hexi.core.message_utils import get_reply_message
|
||||
|
||||
from .. import config
|
||||
from ..services.pipeline import download_image, run_pipeline
|
||||
|
||||
# 两个命令共用同一冷却器(name 相同):同一用户 10 秒内不能连发混淆/解混淆
|
||||
obf_cmd = cooldown(10, hint="处理太频繁了,{secs} 秒后再试", name="cherry_tomato")(
|
||||
on_command("混淆", aliases={"小番茄混淆"}, priority=10, block=True)
|
||||
)
|
||||
deobf_cmd = cooldown(10, hint="处理太频繁了,{secs} 秒后再试", name="cherry_tomato")(
|
||||
on_command("解混淆", aliases={"小番茄解混淆"}, priority=10, block=True)
|
||||
)
|
||||
|
||||
_MD5_RE = re.compile(r"[0-9A-Fa-f]{32}")
|
||||
|
||||
|
||||
@obf_cmd.handle()
|
||||
async def _obfuscate(event: MessageEvent, bot: Bot) -> None:
|
||||
await _handle(event, bot, decrypt=False)
|
||||
|
||||
|
||||
@deobf_cmd.handle()
|
||||
async def _deobfuscate(event: MessageEvent, bot: Bot) -> None:
|
||||
await _handle(event, bot, decrypt=True)
|
||||
|
||||
|
||||
async def _extract_image(
|
||||
event: MessageEvent, bot: Bot
|
||||
) -> tuple[str | None, str | None]:
|
||||
"""取被回复(或当前)消息里第一张图的 (url, file)。
|
||||
|
||||
优先段里的 url;没有 url 时用 file 调 OneBot get_image 现换一个。
|
||||
file 仅用于 offpic 链接失效时的 md5 兜底重试。
|
||||
"""
|
||||
for seg in get_reply_message(event):
|
||||
if seg.type != "image":
|
||||
continue
|
||||
data = seg.data or {}
|
||||
url = data.get("url")
|
||||
if url:
|
||||
return str(url), str(data.get("file") or "")
|
||||
file = data.get("file")
|
||||
if not file:
|
||||
continue
|
||||
try:
|
||||
info = await bot.get_image(file=str(file))
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"cherry_tomato: get_image 取 url 失败: {e!r}")
|
||||
continue
|
||||
url = info.get("url") if isinstance(info, dict) else None
|
||||
if url:
|
||||
return str(url), str(file)
|
||||
return None, None
|
||||
|
||||
|
||||
def _offpic_md5(url: str, file: str | None) -> str | None:
|
||||
"""从 file 字段或 offpic 链接里找 32 位十六进制(md5)串;找不到返回 None。"""
|
||||
for candidate in (file, url):
|
||||
match = _MD5_RE.search(candidate or "")
|
||||
if match:
|
||||
return match.group(0).upper()
|
||||
return None
|
||||
|
||||
|
||||
async def _download_with_fallback(url: str, file: str | None) -> bytes | None:
|
||||
"""下载图片;offpic 老链接失败时用 md5 换 gchat 链接重试一次(picfinder 先例)。"""
|
||||
data = await download_image(url)
|
||||
if data is not None:
|
||||
return data
|
||||
if "c2cpicdw.qpic.cn/offpic_new/" in url:
|
||||
md5 = _offpic_md5(url, file)
|
||||
if md5:
|
||||
retry_url = f"http://gchat.qpic.cn/gchatpic_new/0/0-0-{md5}/0?term=2"
|
||||
logger.info("cherry_tomato: offpic 原链下载失败,改用 gchat 链接重试")
|
||||
data = await download_image(retry_url)
|
||||
return data
|
||||
|
||||
|
||||
async def _handle(event: MessageEvent, bot: Bot, *, decrypt: bool) -> None:
|
||||
action = "解混淆" if decrypt else "混淆"
|
||||
|
||||
url, file = await _extract_image(event, bot)
|
||||
if not url:
|
||||
await UniMessage.text(
|
||||
"请回复一张图片再发这个命令,或把图片和命令一起发~"
|
||||
).finish()
|
||||
|
||||
data = await _download_with_fallback(url, file)
|
||||
if data is None:
|
||||
logger.warning(f"cherry_tomato: 图片下载失败 url={url}")
|
||||
await UniMessage.text("图片下载失败,可能链接已过期,重发一次试试").finish()
|
||||
|
||||
logger.info(f"cherry_tomato: {action}开始 user={event.get_user_id()} url={url}")
|
||||
max_pixels = config.decrypt_max_pixels() if decrypt else config.max_pixels()
|
||||
out, err, notes = await run_pipeline(
|
||||
data, decrypt=decrypt, max_pixels=max_pixels, quality=config.jpeg_quality()
|
||||
)
|
||||
if out is None:
|
||||
logger.warning(f"cherry_tomato: {action}失败: {err}")
|
||||
await UniMessage.text(err or f"{action}失败了,换一张图试试").finish()
|
||||
|
||||
image = UniMessage.image(raw=out)
|
||||
if notes:
|
||||
await (UniMessage.text(";".join(notes) + "\n") + image).send()
|
||||
else:
|
||||
await image.send()
|
||||
Reference in new issue
Block a user