- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
127 lines
3.1 KiB
Python
127 lines
3.1 KiB
Python
import os
|
|
import ssl
|
|
from nonebot import on_command, require, logger
|
|
from nonebot.adapters.onebot.v11 import MessageEvent
|
|
from nonebot.plugin import PluginMetadata
|
|
|
|
from hexi.plugins.nonebot_plugin_hexi_core.message_utils import get_reply_message
|
|
|
|
require("nonebot_plugin_alconna")
|
|
from nonebot_plugin_alconna import UniMessage
|
|
from nonebot.adapters.onebot.v11.bot import Bot
|
|
from nonebot.plugin.on import on_keyword
|
|
import httpx
|
|
from io import BytesIO
|
|
from PIL import Image, ImageSequence
|
|
re_gif = on_keyword({"倒放"})
|
|
|
|
__plugin_meta__ = PluginMetadata(
|
|
name="GIF倒放",
|
|
description="GIF倒放?",
|
|
usage="倒放+图片",
|
|
type="application",
|
|
)
|
|
|
|
@re_gif.handle()
|
|
async def rev_gif(event: MessageEvent, bot: Bot):
|
|
# 情况1,用户对需要倒放的gif进行回复
|
|
messages = get_reply_message(event)
|
|
await match_revgif(messages)
|
|
|
|
|
|
async def match_revgif(messages):
|
|
img_urls = []
|
|
|
|
for message in messages:
|
|
logger.info(f"遍历消息: {message}")
|
|
|
|
# 只处理图片消息
|
|
if message.type != "image":
|
|
continue
|
|
|
|
url = message.data.get("url")
|
|
|
|
if url:
|
|
img_urls.append(url)
|
|
|
|
# 没有图片直接返回
|
|
if not img_urls:
|
|
logger.info("消息中没有图片")
|
|
return
|
|
for url in img_urls:
|
|
logger.info(f"获取到的图片链接:{url}")
|
|
await do_revgif(img_urls)
|
|
|
|
|
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
|
|
|
fd = os.path.dirname(__file__)
|
|
|
|
|
|
async def do_revgif(image_urls):
|
|
gif_img = []
|
|
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
for image_url in image_urls:
|
|
try:
|
|
resp = await client.get(image_url)
|
|
resp.raise_for_status()
|
|
|
|
image = Image.open(BytesIO(resp.content))
|
|
|
|
# 不是gif或只有1帧
|
|
if getattr(image, "n_frames", 1) == 1:
|
|
continue
|
|
|
|
gif_img.append(image)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"下载或解析图片失败: {e}")
|
|
|
|
if not gif_img:
|
|
await UniMessage.text("没有发现GIF图片").send()
|
|
return
|
|
|
|
suit_gifs = []
|
|
|
|
for gif in gif_img:
|
|
if gif.n_frames <= 400:
|
|
suit_gifs.append(gif)
|
|
|
|
if not suit_gifs:
|
|
await UniMessage.text("GIF帧数太多了,懒得倒放😅").send()
|
|
return
|
|
|
|
suit_image = suit_gifs[0]
|
|
|
|
sequence = [f.copy() for f in ImageSequence.Iterator(suit_image)]
|
|
logger.info(f"GIF帧数: {len(sequence)}")
|
|
if len(sequence) <= 2:
|
|
await UniMessage.text("写入文件时发生未知错误").send()
|
|
return
|
|
|
|
await UniMessage.text("ℹ正在翻转图片序列,请稍候").send()
|
|
|
|
sequence.reverse()
|
|
|
|
bio = BytesIO()
|
|
|
|
sequence[0].save(
|
|
bio,
|
|
format="GIF",
|
|
save_all=True,
|
|
append_images=sequence[1:],
|
|
disposal=1,
|
|
loop=0
|
|
)
|
|
|
|
bio.seek(0)
|
|
|
|
await UniMessage.image(raw=bio.getvalue()).send()
|
|
|
|
|
|
def image_to_bytes(image: Image.Image, input_format: str) -> bytes:
|
|
with BytesIO() as bio:
|
|
image.save(bio, format=input_format)
|
|
return bio.getvalue()
|