refactor: restructure plugins per MTSS standard, consolidate assets to res/

Per docs/plugin-audit-report.md (plugins normalized to
Trigger(handlers) → Service(services) → Model(repository/models) + utils):

- Split monolithic __init__.py into handlers/services/utils across
  dailywife, deer_pipe, dice, galgame_card, helldivers_tools,
  huoziyinshua, learning_chat, makeaquote, mc_server_status,
  ncm_saying, picfinder_take, picstatus, random_jm_code, regif,
  steam_info, video_analysis, group_tools
- Move static assets under res/: deer_pipe font/img, makeaquote font,
  helldivers img/templates, huoziyinshua HuoZiYinShua
- Add config.py + register_config_items to ncm_saying, random_jm_code,
  group_tools; learning_chat unified config bridge
- Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in
  dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp
- Disable brash_general_supercredits_tools (stub comment only)
- bot.py: optional stdout/stderr redirect to log file for Web log viewer,
  force ANSI colorize on non-TTY sinks
- Move runtime data (jm_code.json) out of plugin dir into hexi/data
- Docs: plugin-audit-report.md; README reflects removed plugins

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-03 00:44:38 +08:00
co-authored by Claude
parent 25bc30611f
commit 9371a28e35
2389 changed files with 5100 additions and 10127 deletions
+1
View File
@@ -33,6 +33,7 @@ __plugin_meta__ = PluginMetadata(
name="表情裸词操作",
description="恢复“表情 操作词”的裸词用法(对称 右、摸 圆、小丑 爷 等)",
usage="对称 右 / 鬼畜 下 / 摸 圆 / 小丑 爷 / 小丑面具 前",
type="application",
)
try:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
"""brash_general_supercredits_tools — 未完成插件(已停用)。
原作者未完成的「超级积分查询」插件,当前为占位/空壳:
- __init__.py 原为空文件,无任何 matcher / 配置注册。
- 保留 services/、db/ 空目录供后续实现。
⚠️ 本插件处于**禁用**状态:整个文件仅含注释,NoneBot 加载后不注册任何指令。
后续如恢复实现,请按《插件结构标准》/《插件配置标准》补齐 handlers/services/utils 与 config.py。
"""
# ============================================================
# 尚未实现。
# 原项目仅保留了空目录(db/, services/),没有可用逻辑。
# 如要启用,请在此处编写 matcher、服务层与配置注册,并删除下方注释头说明。
# ============================================================
@@ -1,17 +1,6 @@
import base64
import datetime
import hashlib
import json
import os
from random import choice
from tkinter import Image
"""今日老婆 —— 入口(元数据 + 依赖)。"""
from PIL import *
from .utils import *
import httpx
from nonebot import on_command, logger
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
from nonebot import on_command
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
@@ -21,106 +10,4 @@ __plugin_meta__ = PluginMetadata(
type="application",
)
wife = on_command("今日老婆", aliases={"今日老婆"})
def get_member_list(all_list):
id_list = []
for member_list in all_list:
id_list.append(member_list['user_id'])
return id_list
async def download_avatar(user_id: str) -> bytes:
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
data = await download_url(url)
if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e":
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100"
data = await download_url(url)
return data
async def download_url(url: str) -> bytes:
async with httpx.AsyncClient() as client:
for i in range(3):
try:
resp = await client.get(url)
if resp.status_code != 200:
continue
return resp.content
except Exception as e:
logger.error(f"Error downloading {url}, retry {i}/3: {str(e)}")
async def get_wife_info(member_info, qq_id):
img = await download_avatar(qq_id)
avatar = Image.open(BytesIO(img)).convert('RGBA')
avatar = png_resize(avatar, new_width=145, new_height=145)
b_io = BytesIO()
avatar.save(b_io, format="PNG")
avatar_str = 'base64://' + base64.b64encode(b_io.getvalue()).decode()
member_name = (member_info["card"] or member_info["nickname"])
msg = (MessageSegment.text('你今天的群友老婆是:'), MessageSegment.image(avatar_str),
MessageSegment.text(f'{member_name}({qq_id})'))
return msg
def load_group_config(group_id: str) -> int:
filename = os.path.join(os.path.dirname(__file__), 'config', f'{group_id}.json')
try:
with open(filename, encoding='utf8') as f:
config = json.load(f)
return config
except:
return None
def write_group_config(group_id: str, link_id: str, wife_id: str, date: str, config) -> int:
config_file = os.path.join(os.path.dirname(__file__), 'config', f'{group_id}.json')
if config is not None:
config[link_id] = [wife_id, date]
else:
config = {link_id: [wife_id, date]}
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False)
@wife.handle()
async def wife_handle(bot: Bot, ev: MessageEvent):
group_id = ev.group_id
user_id = ev.user_id
bot_id = ev.self_id
wife_id = None
today = str(datetime.date.today())
config = load_group_config(group_id)
# if priv.check_priv(ev, priv.SUPERUSER):
# wife_id = bot_id
if config is not None:
if str(user_id) in list(config):
if config[str(user_id)][1] == today:
wife_id = config[str(user_id)][0]
else:
del config[str(user_id)]
if wife_id is None:
all_list = await bot.get_group_member_list(group_id=group_id)
id_list = get_member_list(all_list)
id_list.remove(bot_id)
id_list.remove(user_id)
if config is not None:
for record_id in list(config):
if config[record_id][1] != today:
del config[record_id]
else:
try:
id_list.remove(int(config[record_id][0]))
except:
del config[record_id]
wife_id = choice(id_list)
write_group_config(group_id, user_id, wife_id, today, config)
member_info = await bot.get_group_member_info(group_id=group_id, user_id=wife_id)
result = await get_wife_info(member_info, wife_id)
await bot.send(ev, result, at_sender=True)
from .handlers import wife # noqa: E402,F401
@@ -0,0 +1,3 @@
"""今日老婆 —— Trigger 层。"""
from .wife import wife # noqa: F401
@@ -0,0 +1,78 @@
"""今日老婆 —— 触发器(Trigger + View)层。"""
from __future__ import annotations
import base64
import datetime
from io import BytesIO
from random import choice
from nonebot import on_command
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
from nonebot.plugin import PluginMetadata
from PIL import Image
from ..services.store import get_member_list, load_group_config, write_group_config
from ..utils import download_avatar, png_resize
__plugin_meta__ = PluginMetadata(
name="今日老婆",
description="随机抓取群友作为老婆",
usage="发送【今日老婆】",
type="application",
)
wife = on_command("今日老婆", aliases={"今日老婆"})
async def get_wife_info(member_info, qq_id):
img = await download_avatar(qq_id)
avatar = Image.open(BytesIO(img)).convert("RGBA")
avatar = png_resize(avatar, new_width=145, new_height=145)
b_io = BytesIO()
avatar.save(b_io, format="PNG")
avatar_str = "base64://" + base64.b64encode(b_io.getvalue()).decode()
member_name = member_info["card"] or member_info["nickname"]
return (
MessageSegment.text("你今天的群友老婆是:"),
MessageSegment.image(avatar_str),
MessageSegment.text(f"{member_name}({qq_id})"),
)
@wife.handle()
async def wife_handle(bot: Bot, ev: MessageEvent):
group_id = ev.group_id
user_id = ev.user_id
bot_id = ev.self_id
wife_id = None
today = str(datetime.date.today())
config = load_group_config(group_id)
if config is not None:
if str(user_id) in list(config):
if config[str(user_id)][1] == today:
wife_id = config[str(user_id)][0]
else:
del config[str(user_id)]
if wife_id is None:
all_list = await bot.get_group_member_list(group_id=group_id)
id_list = get_member_list(all_list)
id_list.remove(bot_id)
id_list.remove(user_id)
if config is not None:
for record_id in list(config):
if config[record_id][1] != today:
del config[record_id]
else:
try:
id_list.remove(int(config[record_id][0]))
except Exception:
del config[record_id]
wife_id = choice(id_list)
write_group_config(group_id, user_id, wife_id, today, config)
member_info = await bot.get_group_member_info(group_id=group_id, user_id=wife_id)
result = await get_wife_info(member_info, wife_id)
await bot.send(ev, result, at_sender=True)
@@ -0,0 +1,3 @@
"""今日老婆 —— Service 层。"""
from .store import get_member_list, load_group_config, write_group_config # noqa: F401
@@ -0,0 +1,35 @@
"""今日老婆 —— 数据访问(Service 层)。"""
from __future__ import annotations
import json
import os
from pathlib import Path
_CONFIG_DIR = Path(__file__).resolve().parents[3] / "data" / "dailywife" / "config"
def get_member_list(all_list):
return [member["user_id"] for member in all_list]
def load_group_config(group_id: str) -> dict | None:
filename = _CONFIG_DIR / f"{group_id}.json"
try:
with open(filename, encoding="utf8") as f:
return json.load(f)
except Exception:
return None
def write_group_config(group_id: str, link_id: str, wife_id: str, date: str, config) -> None:
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config_file = _CONFIG_DIR / f"{group_id}.json"
if config is not None:
config[link_id] = [wife_id, date]
else:
config = {link_id: [wife_id, date]}
tmp = config_file.with_suffix(".tmp")
with open(tmp, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False)
os.replace(tmp, config_file)
@@ -1,50 +0,0 @@
import hashlib
import json
from nonebot.log import logger as sv
import os
import random
import time
from io import BytesIO
import aiohttp
import qrcode
import requests
import requests.exceptions
from PIL import Image, ImageDraw, ImageFont
# PNG重绘大小
def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=''):
"""
PNG缩放透明度处理
:param source_file: 源文件(Image.open())
:param new_width: 设置的宽度
:param new_height: 设置的高度
:param resample: 抗锯齿
:param ref_file: 参考文件
:return:
"""
img = source_file
img = img.convert("RGBA")
width, height = img.size
if ref_file != '':
imgRef = Image.open(ref_file)
new_width, new_height = imgRef.size
else:
if new_height == 0:
new_height = new_width * width / height
bands = img.split()
resample_map = {
"NEAREST": Image.NEAREST,
"BILINEAR": Image.BILINEAR,
"BICUBIC": Image.BICUBIC,
"LANCZOS": Image.LANCZOS
}
resample_method = resample_map.get(resample, Image.LANCZOS) # 默认使用 LANCZOS
bands = [b.resize((new_width, new_height), resample=resample_method) for b in bands]
resized_file = Image.merge('RGBA', bands)
return resized_file
@@ -0,0 +1,3 @@
"""今日老婆 —— 纯工具层。"""
from .avatar import download_avatar, download_url, png_resize # noqa: F401
@@ -0,0 +1,56 @@
"""今日老婆 —— 头像工具(纯工具 + 网络请求)。"""
from __future__ import annotations
import hashlib
from io import BytesIO
import httpx
from PIL import Image
# PNG 重绘大小
def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=""):
img = source_file
img = img.convert("RGBA")
width, height = img.size
if ref_file != "":
imgRef = Image.open(ref_file)
new_width, new_height = imgRef.size
else:
if new_height == 0:
new_height = new_width * width / height
bands = img.split()
resample_map = {
"NEAREST": Image.NEAREST,
"BILINEAR": Image.BILINEAR,
"BICUBIC": Image.BICUBIC,
"LANCZOS": Image.LANCZOS,
}
resample_method = resample_map.get(resample, Image.LANCZOS)
bands = [b.resize((new_width, new_height), resample=resample_method) for b in bands]
return Image.merge("RGBA", bands)
async def download_url(url: str) -> bytes:
from nonebot import logger
async with httpx.AsyncClient() as client:
for i in range(3):
try:
resp = await client.get(url)
if resp.status_code != 200:
continue
return resp.content
except Exception as e:
logger.error(f"Error downloading {url}, retry {i}/3: {str(e)}")
return b""
async def download_avatar(user_id: str) -> bytes:
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
data = await download_url(url)
if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e":
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100"
data = await download_url(url)
return data
@@ -1,12 +1,9 @@
from nonebot import on_command
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
"""打卡记鹿 —— 入口(元数据 + 依赖)。"""
from .data_proc import *
from .img_generator import *
import io
import base64
from nonebot import require
from nonebot.plugin import PluginMetadata
require("nonebot_plugin_orm")
__plugin_meta__ = PluginMetadata(
name="打卡记鹿",
@@ -16,54 +13,4 @@ __plugin_meta__ = PluginMetadata(
type="application",
)
record = on_command("打卡记鹿", aliases={"打卡"}, rule=to_me())
query = on_command("查询记录", aliases={"查卡"})
@record.handle()
async def do_job(bot: Bot, ev: MessageEvent):
# 获取当前日期的年份和月份
now = datetime.now()
year = now.year
month = now.month
# 获取发信人信息
uid = str(ev.user_id)
message_id = ev.message_id
# 添加一条记录
await record_month(uid)
# 获取记录
img = await draw_calendar_grid_image(uid, year, month)
text_msg = MessageSegment.text("这是您的打卡记鹿:\n")
image_msg = MessageSegment.image(pic2b64(img))
mix_msg = (text_msg,)
mix_msg += image_msg
reply_msg = (MessageSegment.reply(message_id),)
reply_msg += mix_msg
await record.finish(reply_msg)
def pic2b64(pic: Image) -> str:
buf = io.BytesIO()
pic.save(buf, format='PNG')
base64_str = base64.b64encode(buf.getvalue()).decode()
return 'base64://' + base64_str
@query.handle()
async def query_job(bot: Bot, ev: MessageEvent):
# 获取当前日期的年份和月份
now = datetime.now()
year = now.year
month = now.month
# 获取发信人信息
uid = str(ev.user_id)
message_id = ev.message_id
# 获取记录
img_file = await draw_calendar_grid_image(uid, year, month)
text_msg = MessageSegment.text("这是您的记鹿:\n")
image_msg = MessageSegment.image(pic2b64(img_file))
mix_msg = (text_msg,)
mix_msg += image_msg
reply_msg = (MessageSegment.reply(message_id),)
reply_msg += mix_msg
await record.finish(reply_msg)
from .handlers import query, record # noqa: E402,F401
@@ -0,0 +1,3 @@
"""打卡记鹿 —— Trigger 层。"""
from .checkin import query, record # noqa: F401
@@ -0,0 +1,58 @@
"""打卡记鹿 —— 触发器(Trigger + View)层。"""
from __future__ import annotations
import base64
import io
from datetime import datetime
from nonebot import on_command
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
from nonebot.rule import to_me
from PIL import Image
from ..services.checkin import checkin, review
record = on_command("打卡记鹿", aliases={"打卡"}, rule=to_me())
query = on_command("查询记录", aliases={"查卡"})
def pic2b64(pic: Image) -> str:
buf = io.BytesIO()
pic.save(buf, format="PNG")
base64_str = base64.b64encode(buf.getvalue()).decode()
return "base64://" + base64_str
@record.handle()
async def do_job(bot: Bot, ev: MessageEvent):
now = datetime.now()
year = now.year
month = now.month
uid = str(ev.user_id)
message_id = ev.message_id
img = await checkin(uid, year, month)
text_msg = MessageSegment.text("这是您的打卡记鹿:\n")
image_msg = MessageSegment.image(pic2b64(img))
mix_msg = (text_msg,)
mix_msg += image_msg
reply_msg = (MessageSegment.reply(message_id),)
reply_msg += mix_msg
await record.finish(reply_msg)
@query.handle()
async def query_job(bot: Bot, ev: MessageEvent):
now = datetime.now()
year = now.year
month = now.month
uid = str(ev.user_id)
message_id = ev.message_id
img_file = await review(uid, year, month)
text_msg = MessageSegment.text("这是您的记鹿:\n")
image_msg = MessageSegment.image(pic2b64(img_file))
mix_msg = (text_msg,)
mix_msg += image_msg
reply_msg = (MessageSegment.reply(message_id),)
reply_msg += mix_msg
await record.finish(reply_msg)
@@ -0,0 +1,23 @@
"""打卡记鹿 —— 数据模型(Model 层)。"""
from __future__ import annotations
from datetime import datetime
from nonebot import require
require("nonebot_plugin_orm")
from nonebot_plugin_orm import Model
from sqlalchemy import BigInteger, Date, Integer
from sqlalchemy.orm import Mapped, mapped_column
class CheckInRecord(Model):
"""打卡记录,一行一次打卡,同一天可多次"""
__tablename__ = "deer_pipe_checkin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
uid: Mapped[int] = mapped_column(BigInteger, index=True)
date: Mapped[datetime.date] = mapped_column(Date, index=True)
@@ -1,25 +1,19 @@
"""打卡记鹿 —— 数据访问(Model/Repository 层)。"""
from __future__ import annotations
from datetime import datetime
from nonebot import require
require("nonebot_plugin_orm")
from nonebot_plugin_orm import Model, get_session
from sqlalchemy import BigInteger, Date, Integer, select
from sqlalchemy.orm import Mapped, mapped_column
from nonebot_plugin_orm import get_session
from sqlalchemy import select
from .models import CheckInRecord
class CheckInRecord(Model):
"""打卡记录,一行一次打卡,同一天可多次"""
__tablename__ = "deer_pipe_checkin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
uid: Mapped[int] = mapped_column(BigInteger, index=True)
date: Mapped[datetime.date] = mapped_column(Date, index=True)
# 记录信息
async def record_month(uid):
now = datetime.now()
async with get_session() as session:
@@ -27,7 +21,6 @@ async def record_month(uid):
await session.commit()
# 读取当月信息(返回当月所有打卡的日,同一天多次打卡会有重复项)
async def get_records(uid):
now = datetime.now()
month_start = datetime(now.year, now.month, 1).date()

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,3 @@
"""打卡记鹿 —— Service 层。"""
from .checkin import checkin, review # noqa: F401
@@ -0,0 +1,19 @@
"""打卡记鹿 —— 业务编排(Service 层)。"""
from __future__ import annotations
from PIL import Image
from ..repository import record_month
from ..utils.render import draw_calendar_grid_image
async def checkin(uid: str, year: int, month: int) -> Image.Image:
"""记录一次打卡并返回当月日历图。"""
await record_month(uid)
return await draw_calendar_grid_image(uid, year, month)
async def review(uid: str, year: int, month: int) -> Image.Image:
"""仅查询(不新增记录)并返回当月日历图。"""
return await draw_calendar_grid_image(uid, year, month)
@@ -0,0 +1,3 @@
"""打卡记鹿 —— 渲染工具。"""
from .render import draw_calendar_grid_image # noqa: F401
@@ -1,121 +1,87 @@
import calendar
from collections import Counter
import os
from datetime import datetime, timedelta
from PIL import Image, ImageDraw, ImageFont, ImageOps
from .data_proc import *
import random
"""打卡记鹿 —— 渲染工具(utils 层)。"""
filepath = os.path.dirname(__file__).replace("\\", "/")
from __future__ import annotations
import calendar
import os
import random
from collections import Counter
from datetime import datetime, timedelta
from nonebot.log import logger
from PIL import Image, ImageDraw, ImageFont, ImageOps
from ..repository import get_records
# 资源统一放插件 res/ 下
filepath = os.path.join(os.path.dirname(os.path.dirname(__file__)), "res").replace("\\", "/")
async def draw_calendar_grid_image(uid, year, month):
# 获取该月的第一天是星期几,以及这个月有多少天
first_weekday, num_days = calendar.monthrange(year, month)
# 调整第一天的索引,使周日对应0,周六对应6
first_weekday = (first_weekday + 1) % 7
# 设置单元格尺寸和内边距
cell_size = 50
cell_padding = 5
# 计算当前月份的行数
num_rows = (num_days + first_weekday + 6) // 7 + 1 # +1 行用于留空
# 计算图片宽度和高度
num_rows = (num_days + first_weekday + 6) // 7 + 1
image_width = 7 * (cell_size + cell_padding) - cell_padding
image_height = num_rows * (cell_size + cell_padding) - cell_padding
# 创建图像对象
img = Image.new("RGBA", (image_width, image_height), "white")
draw = ImageDraw.Draw(img)
font_path = filepath + "/font/SourceHanSansCN-Medium.otf"
print(font_path)
logger.debug(f"打卡字体路径: {font_path}")
font_normal = ImageFont.truetype(font=font_path, size=18)
font_large = ImageFont.truetype(font=font_path, size=24)
# 绘制网格线
for i in range(7):
x = i * (cell_size + cell_padding)
draw.line([(x, 55), (x, image_height)], fill="black", width=1)
for j in range(0, image_height + cell_size, cell_size + cell_padding):
draw.line([(0, j), (image_width, j)], fill="black", width=1)
# 小🦌
deer_pipe_path = filepath + "/img/deer_pipe.jpg"
deer_pipe_img = Image.open(deer_pipe_path)
deer_pipe_img = deer_pipe_img.resize((54, 50))
# 画标题
title = f"{month}月打卡记鹿"
text_length = draw.textlength(text=title, font=font_large)
img_xy = (int(((image_width - text_length + 55) / 2) - 55), 5)
title_pos = ((image_width - text_length + 55) / 2, 10)
img.paste(deer_pipe_img, img_xy)
draw.text(title_pos, title, fill="black", font=font_large)
# 获取🦌信息(同一天多次打卡会得到重复项)
day_counts = Counter(await get_records(uid))
# 填充日期
date = datetime(year, month, 1)
for day in range(1, num_days + 1):
weekday = (first_weekday + day - 1) % 7
row = (first_weekday + day - 1) // 7 + 1 # +1 行用于留空
row = (first_weekday + day - 1) // 7 + 1
x = weekday * (cell_size + cell_padding) + cell_padding
y = row * (cell_size + cell_padding) + cell_padding
img.paste(deer_pipe_img, (x - 4, y))
draw.text((x+20, y - 2), f"{str(day)}", fill="black", font=font_normal)
# 🦌是天的标记,打卡的PNG印记才是要多次绘制的对象,横向错开
draw.text((x + 20, y - 2), f"{str(day)}", fill="black", font=font_normal)
for i in range(day_counts.get(day, 0)):
im = get_random_right()
img = image_paste(im, img, (x - 4 + i * 10, y))
# 保存图片
# img.save(output_image_path)
# 显示图片
# img.show()
img = ImageOps.expand(img, border=5, fill="black")
img = ImageOps.expand(img, border=5, fill="white")
return img
# 全局变量,用于存储已经选择过的文件名
selected_images = []
def get_random_right():
global selected_images
path = filepath + "/img/right"
# 过滤掉已经选择过的文件名
im_name = [name for name in os.listdir(path) if name not in selected_images]
# 如果所有文件都已经选择过,重新初始化已选择列表
if not im_name:
selected_images = []
im_name = os.listdir(path)
index = random.randint(0, len(im_name) - 1)
im = im_name[index]
selected_images.append(im) # 将选择的文件名添加到已选择列表中
selected_images.append(im)
im_path = os.path.join(path, im)
im_file = Image.open(im_path)
im_file = im_file.resize((50, 50))
return im_file
# 图片粘贴
def image_paste(paste_image, under_image, pos):
"""
:param paste_image: 需要粘贴的图片
:param under_image: 底图
:param pos: 位置(x,y)坐标
:return: 返回图片
"""
# 获取需要贴入图片的透明通道
r, g, b, alpha = paste_image.split()
# 粘贴时将alpha值传递至mask属性
under_image.paste(paste_image, pos, alpha)
return under_image
+3 -72
View File
@@ -1,13 +1,9 @@
import random
import re
"""掷骰子 / 结婚判定"""
from nonebot import on_regex, on_startswith, require
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot import require
from nonebot.plugin import PluginMetadata
from nonebot.typing import T_State
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage
__plugin_meta__ = PluginMetadata(
name="掷骰子",
@@ -16,69 +12,4 @@ __plugin_meta__ = PluginMetadata(
type="application",
)
dice = on_regex(
re.compile(
r"^\.r\s*(?:(?P<num>\d{0,2})d(?:(?P<min>\d{1,4})~)?(?P<max>\d{0,4})(?:(?P<opr>[+-])(?P<offset>\d{0,5}))?)?\b",
re.I,
)
)
marriage = on_startswith(".qj")
async def do_dice(
ev: MessageEvent,
num: int,
min_: int,
max_: int,
opr: int,
offset: int,
tip: str = "的掷骰结果是:",
) -> None:
if num == 0:
await UniMessage.text("咦?我骰子呢?").send()
return
min_, max_ = min(min_, max_), max(min_, max_)
rolls = [random.randint(min_, max_) for _ in range(num)]
total = sum(rolls)
rolls_str = "+".join(map(str, rolls))
if len(rolls_str) > 100:
rolls_str = str(total)
res = total + opr * offset
sign = " +-"[opr]
msg = [
f"{tip}\n",
str(num) if num > 1 else "",
"D",
f"{min_}~" if min_ != 1 else "",
str(max_),
f"{sign}{offset}" if offset else "",
f"={rolls_str}",
f"{sign}{offset}" if offset else "",
f"={res}" if offset or num > 1 else "",
]
await UniMessage.text("".join(msg)).send(at_sender=True)
@dice.handle()
async def dice_handle(ev: MessageEvent, state: T_State):
num, min_, max_, opr, offset = 1, 1, 100, 1, 0
matched = state.get("_matched_dict") or {}
if s := matched.get("num"):
num = int(s)
if s := matched.get("min"):
min_ = int(s)
if s := matched.get("max"):
max_ = int(s)
if s := matched.get("opr"):
opr = -1 if s == "-" else 1
if s := matched.get("offset"):
offset = int(s)
await do_dice(ev, num, min_, max_, opr, offset)
@marriage.handle()
async def marriage_handle(ev: MessageEvent):
text = ev.message.extract_plain_text().strip()
wife = text[len(".qj") :].strip()
tip = f"与{wife}的ケッコンカッコカリ结果是:" if wife else "的ケッコンカッコカリ结果是:"
await do_dice(ev, 1, 3, 6, 1, 0, tip)
from .handlers import dice, marriage # noqa: E402,F401
@@ -0,0 +1,3 @@
"""掷骰子 —— Trigger 层。"""
from .roll import dice, marriage # noqa: F401
@@ -0,0 +1,49 @@
"""掷骰子 —— 触发器(Trigger)层。"""
from __future__ import annotations
import re
from nonebot import on_regex, on_startswith, require
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot.typing import T_State
from nonebot_plugin_alconna import UniMessage
from ..services.dice import build_dice_message
require("nonebot_plugin_alconna")
dice = on_regex(
re.compile(
r"^\.r\s*(?:(?P<num>\d{0,2})d(?:(?P<min>\d{1,4})~)?(?P<max>\d{0,4})(?:(?P<opr>[+-])(?P<offset>\d{0,5}))?)?\b",
re.I,
)
)
marriage = on_startswith(".qj")
@dice.handle()
async def dice_handle(ev: MessageEvent, state: T_State):
num, min_, max_, opr, offset = 1, 1, 100, 1, 0
matched = state.get("_matched_dict") or {}
if s := matched.get("num"):
num = int(s)
if s := matched.get("min"):
min_ = int(s)
if s := matched.get("max"):
max_ = int(s)
if s := matched.get("opr"):
opr = -1 if s == "-" else 1
if s := matched.get("offset"):
offset = int(s)
await UniMessage.text(build_dice_message(num, min_, max_, opr, offset)).send(
at_sender=True
)
@marriage.handle()
async def marriage_handle(ev: MessageEvent):
text = ev.message.extract_plain_text().strip()
wife = text[len(".qj") :].strip()
tip = f"与{wife}的ケッコンカッコカリ结果是:" if wife else "的ケッコンカッコカリ结果是:"
await UniMessage.text(build_dice_message(1, 3, 6, 1, 0, tip)).send(at_sender=True)
@@ -0,0 +1,3 @@
"""掷骰子 —— Service 层。"""
from .dice import build_dice_message # noqa: F401
@@ -0,0 +1,38 @@
"""掷骰子 —— 业务逻辑(Service 层,不依赖 nonebot,可单测)。"""
from __future__ import annotations
import random
def build_dice_message(
num: int,
min_: int,
max_: int,
opr: int,
offset: int,
tip: str = "的掷骰结果是:",
) -> str:
if num == 0:
return "咦?我骰子呢?"
min_, max_ = min(min_, max_), max(min_, max_)
rolls = [random.randint(min_, max_) for _ in range(num)]
total = sum(rolls)
rolls_str = "+".join(map(str, rolls))
if len(rolls_str) > 100:
rolls_str = str(total)
res = total + opr * offset
sign = " -+"[opr]
return "".join(
[
f"{tip}\n",
str(num) if num > 1 else "",
"D",
f"{min_}~" if min_ != 1 else "",
str(max_),
f"{sign}{offset}" if offset else "",
f"={rolls_str}",
f"{sign}{offset}" if offset else "",
f"={res}" if offset or num > 1 else "",
]
)
@@ -1,37 +1,25 @@
"""群聊人设卡 —— 基于群聊语料蒸馏成员形象,生成 galgame 风格人物卡
消息入口:on_message 监听全部消息事件,类型注解过滤出群聊,
两级闸门(群开关 + 个人 opt-in)通过后,治理层处理落库。
消息入口:on_message 监听全部消息事件,类型注解过滤出群聊,两级闸门(群开关 +
个人 opt-in)通过后,治理层处理落库。采集编排已下沉到 services/collector.py。
当前进度:数据层 + 采集层(消息路径)完成;总结路径(印象/画像)待实现。
"""
import json
import time
from collections import deque
from typing import Optional
from nonebot import get_driver, on_message, require
from nonebot import get_driver, require
require("nonebot_plugin_orm")
from nonebot.adapters.onebot.v11 import GroupMessageEvent
from nonebot.adapters.onebot.v11 import GroupMessageEvent # noqa: F401
from nonebot.plugin import PluginMetadata
# ── 禁用 nonebot_plugin_orm 的启动自动同步 ─────────────────────────────
# .env 里 ALEMBIC_STARTUP_CHECK=false 时,orm 插件每次启动都会 autogenerate
# 同步数据库模式;模型一有变更(改 models.py)就会清空整表数据(已实测踩坑)。
# 本插件的 schema 演进统一走 repository.ensure_schema() 显式迁移,
# 因此把 migrate.sync 替换为安全空操作。改动必须在插件导入期完成
# (早于 orm 的 on_startup 钩子执行)。
from nonebot import logger as _logger
try:
import nonebot_plugin_orm.migrate as _orm_migrate
def _safe_sync(*_args, **_kwargs):
# 同步函数:orm 用 greenlet_spawn 同步调用 migrate.sync,
# 若写成 async def 会产生 never-awaited coroutine 警告
_logger.debug("galgame_card | 已禁用 orm 自动同步(schema 演进由 ensure_schema 接管)")
_orm_migrate.sync = _safe_sync
@@ -41,6 +29,7 @@ except ImportError:
from . import models # noqa: F401 # 确保表结构注册
from . import repository, processor
from . import web # noqa: F401 # Web 管理后台(/galgame_card)
from . import handlers as _handlers # noqa: E402
from hexi.web_config import register_plugin_config # noqa: E402
from hexi.web_plugin_registry import register_web_plugin # noqa: E402
@@ -48,9 +37,7 @@ from . import web_hub # noqa: E402
def _apply_galgame_config(values: dict) -> None:
"""Web 保存后热刷新:直接改 config 模块的 pydantic 对象,无需重启。"""
import sys
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
if conf_mod is None:
return
@@ -65,9 +52,7 @@ def _apply_galgame_config(values: dict) -> None:
def _get_galgame_config_values() -> dict:
"""Web 表单回填当前生效值。"""
import sys
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
conf = getattr(conf_mod, "config", None) if conf_mod else None
if conf is None:
@@ -104,32 +89,9 @@ __plugin_meta__ = PluginMetadata(
name="群聊人设卡",
description="基于群聊语料蒸馏成员形象,生成 galgame 风格人物卡",
usage="开发中……",
type="application",
)
# 采样/复读检测的内存状态(不进库)
_recent_texts: dict[int, deque[str]] = {} # group_id -> 最近消息文本
_last_store: dict[tuple[int, int], float] = {} # (group_id, user_id) -> 上次落库时间戳
def _is_repeat(group_id: int, content: str) -> bool:
"""复读检测:内容在最近 N 条里出现过则丢弃"""
queue = _recent_texts.setdefault(group_id, deque(maxlen=processor.REPEAT_WINDOW))
if content in queue:
return True
queue.append(content)
return False
def _throttled(group_id: int, user_id: int) -> bool:
"""刷屏采样:5 秒内同一个人只落库一次"""
key = (group_id, user_id)
now = time.time()
if now - _last_store.get(key, 0) < processor.THROTTLE_SECONDS:
return True
_last_store[key] = now
return False
_driver = get_driver()
@@ -137,51 +99,3 @@ _driver = get_driver()
async def _ensure_schema():
"""老库迁移(persona_group.group_name 列),幂等"""
await repository.ensure_schema()
collector = on_message(block=False) # 监听全部消息事件,不拦截其他插件
@collector.handle()
async def collect(event: GroupMessageEvent):
# 鉴权:两级闸门(群开关 → 个人 opt-in)
if not await repository.is_group_enabled(event.group_id):
return
if not await repository.is_joined(event.user_id, event.group_id):
return
# 机器人自己(如转发)不收
if event.user_id == event.self_id:
return
command_starts = get_driver().config.command_start
# 治理:文本 / 图片 / 表情任一存在才记录
if not processor.has_recordable_content(event):
return
text = processor.filter_content(
processor.extract_text(event), command_starts=command_starts
)
image_hashes = processor.extract_image_hashes(event)
content = processor.build_content(
text or "", len(image_hashes), processor.count_faces(event)
)
if content is None:
return
# 脱敏:敏感值替换为占位符(语境 = 本群最近几条已治理文本,用于"验证码/密码"类强化)
context = " ".join(_recent_texts.get(event.group_id, ()))
content = processor.desensitize(content, context)
# 采样:复读 + 刷屏
if _is_repeat(event.group_id, content):
return
if _throttled(event.group_id, event.user_id):
return
await repository.add_chat_log(
user_id=event.user_id,
group_id=event.group_id,
content=content,
nickname=event.sender.card or event.sender.nickname or "",
target_user_id=processor.extract_target_user_id(event),
reply_to_content=processor.extract_reply_to_content(event),
image_count=len(image_hashes),
image_hashes=json.dumps(image_hashes, ensure_ascii=False) if image_hashes else None,
)
@@ -0,0 +1,3 @@
"""群聊人设卡 —— Trigger 层。"""
from .collector import collector # noqa: F401
@@ -0,0 +1,15 @@
"""群聊人设卡 —— 触发器(Trigger)层。"""
from __future__ import annotations
from nonebot import on_message
from nonebot.adapters.onebot.v11 import GroupMessageEvent
from ..services.collector import collect
collector = on_message(block=False) # 监听全部消息事件,不拦截其他插件
@collector.handle()
async def _collect(event: GroupMessageEvent):
await collect(event)
@@ -0,0 +1,3 @@
"""群聊人设卡 —— Service 层。"""
from .collector import collect # noqa: F401
@@ -0,0 +1,77 @@
"""群聊人设卡 —— 采集编排(Service 层)。
把 __init__.py 里的采样/复读/刷屏检测与落库编排抽到此处,使入口更薄。
"""
from __future__ import annotations
import json
import time
from collections import deque
from nonebot import get_driver
from .. import processor, repository
# 采样/复读检测的内存状态(不进库)
_recent_texts: dict[int, deque[str]] = {} # group_id -> 最近消息文本
_last_store: dict[tuple[int, int], float] = {} # (group_id, user_id) -> 上次落库时间戳
def _is_repeat(group_id: int, content: str) -> bool:
"""复读检测:内容在最近 N 条里出现过则丢弃"""
queue = _recent_texts.setdefault(group_id, deque(maxlen=processor.REPEAT_WINDOW))
if content in queue:
return True
queue.append(content)
return False
def _throttled(group_id: int, user_id: int) -> bool:
"""刷屏采样:5 秒内同一个人只落库一次"""
key = (group_id, user_id)
now = time.time()
if now - _last_store.get(key, 0) < processor.THROTTLE_SECONDS:
return True
_last_store[key] = now
return False
async def collect(event):
"""鉴权 + 治理 + 采样 + 落库。"""
if not await repository.is_group_enabled(event.group_id):
return
if not await repository.is_joined(event.user_id, event.group_id):
return
if event.user_id == event.self_id:
return
command_starts = get_driver().config.command_start
if not processor.has_recordable_content(event):
return
text = processor.filter_content(
processor.extract_text(event), command_starts=command_starts
)
image_hashes = processor.extract_image_hashes(event)
content = processor.build_content(
text or "", len(image_hashes), processor.count_faces(event)
)
if content is None:
return
context = " ".join(_recent_texts.get(event.group_id, ()))
content = processor.desensitize(content, context)
if _is_repeat(event.group_id, content):
return
if _throttled(event.group_id, event.user_id):
return
await repository.add_chat_log(
user_id=event.user_id,
group_id=event.group_id,
content=content,
nickname=event.sender.card or event.sender.nickname or "",
target_user_id=processor.extract_target_user_id(event),
reply_to_content=processor.extract_reply_to_content(event),
image_count=len(image_hashes),
image_hashes=json.dumps(image_hashes, ensure_ascii=False) if image_hashes else None,
)
@@ -30,4 +30,7 @@ __plugin_meta__ = PluginMetadata(
type="application",
)
from . import config as _config # noqa: E402,F401
from .handlers import chat, management, notices, repeater # noqa: E402,F401
_config.register_config()
@@ -0,0 +1,32 @@
"""group_tools —— 配置注册(按《插件配置文件标准》)。
当前插件已具备 handlers/services/utils 分层;此处仅把模块级常量(如违禁词)
接入统一 Web 配置,使 Web 管理台(/hub)可读可改。
"""
from __future__ import annotations
import sys as _sys
from hexi.config_standard import register_config_items
from . import constants
# plugin_id = NoneBot 插件模块名
_PLUGIN_ID = __package__
def register_config() -> None:
register_config_items(
_PLUGIN_ID,
[
{
"key": "BANNED_WORDS",
"label": "违禁词",
"type": "text",
"item_type": "str",
"default": list(constants.BANNED_WORDS),
},
],
store=_sys.modules[constants.__name__],
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,3 @@
"""聊天类触发(handlers/chat)。"""
from . import greet, reaction, voice # noqa: F401
@@ -1,21 +1,20 @@
import random
"""群管理工具 —— 聊天互动(问候/尬聊)。"""
from __future__ import annotations
from nonebot import get_driver, on_command, on_fullmatch, on_keyword, require
from nonebot.adapters.onebot.v11 import Bot, MessageEvent
from nonebot.rule import to_me
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage
from ..constants import BANNED_WORDS, RES_DIR
from ..services.moderation import silence
from ..utils.media import image_to_base64, random_audio
from ...constants import RES_DIR
from ...services.moderation import silence
from ...utils.media import image_to_base64
require("nonebot_plugin_alconna")
SUPERUSERS = get_driver().config.superusers
# ---------- 基础打招呼 ----------
say_hello = on_command(
"zai?",
aliases={"在?", "在?", "在吗", "在么?", "在嘛", "在嘛?"},
@@ -29,8 +28,6 @@ async def say_hello_handle(ev: MessageEvent):
await UniMessage.text("はい!私はいつも貴方の側にいますよ!").send()
# ---------- 关键词/整句互动 ----------
say_sorry = on_fullmatch("沙雕机器人")
@@ -95,70 +92,3 @@ nihaole = on_fullmatch("我好了")
@nihaole.handle()
async def nihaole_handle(bot: Bot, ev: MessageEvent):
await UniMessage.text("不许好,憋回去!").send()
# await silence(bot, ev, 30)
play_zhanshi = on_keyword({"优质的战士"})
@play_zhanshi.handle()
async def play_zhanshi_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "youzhizhanshi")
if base64_str:
await UniMessage.voice(url=base64_str).send()
play_ice = on_fullmatch("溜冰")
@play_ice.handle()
async def play_ice_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "ice")
if base64_str:
await UniMessage.voice(url=base64_str).send()
play_dxl = on_fullmatch("罕见")
@play_dxl.handle()
async def play_dxl_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "dxl")
if base64_str:
await UniMessage.voice(url=base64_str).send()
chat_queshi = on_keyword({"确实", "有一说一", "u1s1", "yysy"})
@chat_queshi.handle()
async def chat_queshi_handle(ev: MessageEvent):
if random.random() < 0.05:
base64_str = image_to_base64(RES_DIR / "img" / "确实.jpg")
if base64_str:
await UniMessage.image(url=base64_str).send()
chat_neigui = on_keyword({"内鬼"})
@chat_neigui.handle()
async def chat_neigui_handle(ev: MessageEvent):
if random.random() < 0.10:
base64_str = image_to_base64(RES_DIR / "img" / "内鬼.png")
if base64_str:
await UniMessage.image(url=base64_str).send()
# ---------- 违禁词黑手语音 ----------
banned_word_voice = on_command(
"ban_word", aliases=set(BANNED_WORDS), rule=to_me(), block=True
)
@banned_word_voice.handle()
async def banned_word_voice_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "heishou")
if base64_str:
await UniMessage.voice(url=base64_str).send()
@@ -0,0 +1,37 @@
"""群管理工具 —— 聊天互动(概率表情)。"""
from __future__ import annotations
import random
from nonebot import require
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot_plugin_alconna import UniMessage
from nonebot import on_keyword
from ...constants import RES_DIR
from ...utils.media import image_to_base64
require("nonebot_plugin_alconna")
chat_queshi = on_keyword({"确实", "有一说一", "u1s1", "yysy"})
@chat_queshi.handle()
async def chat_queshi_handle(ev: MessageEvent):
if random.random() < 0.05:
base64_str = image_to_base64(RES_DIR / "img" / "确实.jpg")
if base64_str:
await UniMessage.image(url=base64_str).send()
chat_neigui = on_keyword({"内鬼"})
@chat_neigui.handle()
async def chat_neigui_handle(ev: MessageEvent):
if random.random() < 0.10:
base64_str = image_to_base64(RES_DIR / "img" / "内鬼.png")
if base64_str:
await UniMessage.image(url=base64_str).send()
@@ -0,0 +1,55 @@
"""群管理工具 —— 聊天互动(语音)。"""
from __future__ import annotations
from nonebot import on_command, on_fullmatch, on_keyword, require
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot.rule import to_me
from nonebot_plugin_alconna import UniMessage
from ...constants import BANNED_WORDS, RES_DIR
from ...utils.media import random_audio
require("nonebot_plugin_alconna")
play_zhanshi = on_keyword({"优质的战士"})
@play_zhanshi.handle()
async def play_zhanshi_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "youzhizhanshi")
if base64_str:
await UniMessage.voice(url=base64_str).send()
play_ice = on_fullmatch("溜冰")
@play_ice.handle()
async def play_ice_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "ice")
if base64_str:
await UniMessage.voice(url=base64_str).send()
play_dxl = on_fullmatch("罕见")
@play_dxl.handle()
async def play_dxl_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "dxl")
if base64_str:
await UniMessage.voice(url=base64_str).send()
banned_word_voice = on_command(
"ban_word", aliases=set(BANNED_WORDS), rule=to_me(), block=True
)
@banned_word_voice.handle()
async def banned_word_voice_handle(ev: MessageEvent):
base64_str = random_audio(RES_DIR / "record" / "voice" / "heishou")
if base64_str:
await UniMessage.voice(url=base64_str).send()
@@ -1,399 +0,0 @@
import asyncio
import math
import random
import re
import time
from nonebot import logger, on_fullmatch, on_message, on_regex, on_startswith, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent
from nonebot.rule import Rule, is_type
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from ..constants import IMG_DIR, TEMP_DIR
from ..services.moderation import group_silence, member_silence, silence
from ..services.profile import card_edit, group_name, title_get
from ..services.video import download_video, get_video_info, local_video
from ..utils.media import image_to_base64
from ..utils.message import get_at_id, has_at_all
from ..utils.permissions import check_perm, honor_info, self_member_info
from ..utils.text import is_number, parse_seconds, strip_prefix
# ============ 管理员与公告 ============
set_admin = on_startswith("设置管理员")
unset_admin = on_startswith("取消管理员")
set_group_notice = on_startswith("设置群公告")
@set_admin.handle()
async def set_admin_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_perm(bot, ev):
return
for seg in ev.message:
if seg.type == "at" and seg.data.get("qq") != "all":
await bot.set_group_admin(
group_id=ev.group_id, user_id=int(seg.data["qq"]), enable=True
)
await UniMessage.text("我好了").send()
@unset_admin.handle()
async def unset_admin_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_perm(bot, ev):
return
for seg in ev.message:
if seg.type == "at" and seg.data.get("qq") != "all":
await bot.set_group_admin(
group_id=ev.group_id, user_id=int(seg.data["qq"]), enable=False
)
await UniMessage.text("我好了").send()
@set_group_notice.handle()
async def set_group_notice_handle(bot: Bot, ev: GroupMessageEvent):
content = ev.message.extract_plain_text().removeprefix("设置群公告").strip()
if not content:
return
if not await check_perm(bot, ev):
return
await bot.call_api("_send_group_notice", group_id=ev.group_id, content=content)
await UniMessage.text("我好了").send()
# ============ 禁言相关 ============
def _is_ban_cmd(event: GroupMessageEvent) -> bool:
"""分钟制禁言命令:"禁言@xxx 5" 表示禁言 5 分钟;排除秒制的 "禁言一下" """
text = event.message.extract_plain_text()
return text.startswith("禁言") and not text.startswith("禁言一下")
ban_user = on_message(rule=Rule(_is_ban_cmd) & is_type(GroupMessageEvent), block=True)
ban_member = on_startswith(("来发口球", "塞口球", "禁言一下"))
one_for_one = on_startswith("极限一换一")
cancel_ban = on_startswith(("解除口球", "取消口球", "摘口球", "脱口球", "取消禁言", "解除禁言", "解禁"))
ban_all = on_fullmatch(("全员口球", "全员禁言"))
cancel_ban_all = on_fullmatch(("取消全员口球", "取消全员禁言", "解除全员口球", "解除全员禁言"))
sleep_8h = on_fullmatch(("睡眠套餐", "休眠套餐", "精致睡眠", "来一份精致睡眠套餐"))
tea = on_regex(r"(来|來)(.*(份|个)(.*)(睡|茶)(.*))套餐")
@ban_user.handle()
async def ban_user_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_perm(bot, ev, silent=True):
return
msg = ev.message.extract_plain_text().removeprefix("禁言").strip()
at_seg = next(
(seg for seg in ev.message if seg.type == "at" and seg.data.get("qq") != "all"),
None,
)
if not at_seg:
return
try:
uid = int(at_seg.data["qq"])
minutes = 1 if not msg else int(msg)
await bot.set_group_ban(
group_id=ev.group_id, user_id=uid, duration=minutes * 60
)
await UniMessage.text("我好了").send()
except Exception as ex:
logger.error(ex)
@ban_member.handle()
async def ban_member_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
duration_text = strip_prefix(
ev.message.extract_plain_text(), ("来发口球", "塞口球", "禁言一下")
)
if not duration_text:
duration_text = "30"
if has_at_all(ev):
await group_silence(bot, ev, gid, True)
return
sid = get_at_id(ev) or uid
await member_silence(bot, ev, uid, sid, gid, duration_text, silent=False)
@one_for_one.handle()
async def one_for_one_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
duration_text = strip_prefix(ev.message.extract_plain_text(), ("极限一换一",))
if has_at_all(ev):
await UniMessage.text("人干事?").send(at_sender=True)
await member_silence(bot, ev, uid, uid, gid, "180", silent=False)
return
seconds = parse_seconds(duration_text)
if seconds is None:
await UniMessage.text("请输入正确时长~").send(at_sender=True)
return
if seconds > 30:
await UniMessage.text("时长不可大于30秒哟~").send(at_sender=True)
return
sid = get_at_id(ev) or uid
await member_silence(bot, ev, uid, sid, gid, str(seconds), silent=True)
await member_silence(bot, ev, uid, uid, gid, str(seconds), silent=True)
await (
UniMessage.at(user_id=str(uid))
.text("成功一换一")
.at(user_id=str(sid))
.text(f"{seconds}秒~")
.send()
)
@cancel_ban.handle()
async def cancel_ban_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
uid = ev.user_id
gid = ev.group_id
if has_at_all(ev):
await group_silence(bot, ev, gid, False)
return
sid = get_at_id(ev)
if sid is None:
await UniMessage.text("请@需要摘口球的群员哦w").send()
return
await member_silence(bot, ev, uid, sid, gid, "0", silent=False)
@ban_all.handle()
async def ban_all_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
await group_silence(bot, ev, ev.group_id, True)
@cancel_ban_all.handle()
async def cancel_ban_all_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
await group_silence(bot, ev, ev.group_id, False)
@sleep_8h.handle()
async def sleep_8h_handle(bot: Bot, ev: MessageEvent):
await silence(bot, ev, 8 * 60 * 60)
@tea.handle()
async def tea_handle(bot: Bot, ev: MessageEvent):
base = 0 if "午" in ev.plain_text else 5 * 60 * 60
length = len(ev.plain_text)
sleep_time = base + round(
math.sqrt(length) * 60 * 30 + 60 * random.randint(-15, 15)
)
await silence(bot, ev, sleep_time)
# ============ 头衔 / 名片 / 群名 ============
special_title = on_startswith("申请头衔")
del_special_title = on_fullmatch(("删除头衔", "清除头衔", "收回头衔", "回收头衔"))
card_set = on_startswith(("修改名片", "修改群名片", "设置名片", "设置群名片"))
set_group_name = on_startswith(("修改群名", "设置群名"))
@special_title.handle()
async def special_title_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
title = strip_prefix(ev.message.extract_plain_text(), ("申请头衔",))
sid = get_at_id(ev) or uid
await title_get(bot, ev, uid, sid, gid, title)
# go-cqhttp似乎暂时不支持收回专属头衔...
@del_special_title.handle()
async def del_special_title_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
sid = get_at_id(ev) or uid
await title_get(bot, ev, uid, sid, gid, None)
@card_set.handle()
async def card_set_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
card_text = strip_prefix(
ev.message.extract_plain_text(), ("修改名片", "修改群名片", "设置名片", "设置群名片")
)
sid = get_at_id(ev) or uid
await card_edit(bot, ev, uid, sid, gid, card_text)
@set_group_name.handle()
async def set_group_name_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
gid = ev.group_id
name_text = strip_prefix(ev.message.extract_plain_text(), ("修改群名", "设置群名"))
await group_name(bot, ev, gid, name_text)
# ============ 龙王 ============
whois_dragon = on_fullmatch(("谁是龙王", "迫害龙王", "龙王是谁"))
dragon_list = on_fullmatch(("龙王榜", "龙王排行", "龙王排行榜", "龙王榜单"))
@whois_dragon.handle()
async def whois_dragon_handle(bot: Bot, ev: GroupMessageEvent):
gid = ev.group_id
self_info = await self_member_info(bot, gid)
sid = self_info["user_id"]
ta_info = await honor_info(bot, gid, "talkative")
if not ta_info or not ta_info.get("current_talkative"):
await UniMessage.text("本群没有开启龙王标志哦~").send()
return
dk = ta_info["current_talkative"]["user_id"]
if sid == dk:
pic = image_to_base64(IMG_DIR / "dk_is_me.jpg")
await (
UniMessage.text("你们这群丢人玩意,龙王怎么又是我\n")
.image(url=pic)
.send()
)
else:
action = random.choice(["龙王出来挨透", "龙王出来喷水"])
dk_avatar = ta_info["current_talkative"]["avatar"] + "640" + f"&t={dk}"
await (
UniMessage.at(user_id=str(dk))
.text(f"\n{action}\n")
.image(url=dk_avatar)
.send()
)
@dragon_list.handle()
async def dragon_list_handle(bot: Bot, ev: GroupMessageEvent):
gid = ev.group_id
ta_info = await honor_info(bot, gid, "talkative")
if not ta_info or not ta_info.get("talkative_list"):
await UniMessage.text("本群暂时没有龙王数据哦~").send()
return
dragon_king_list_info = ta_info["talkative_list"]
# 处理description和nickname关系
dkdir = {}
for item in dragon_king_list_info:
user_id = item["user_id"]
nickname = item["nickname"]
description = item["description"]
temp = re.findall(r"\d+", description)
# 累计获得龙王的次数
dragon_king_days = int(temp[0])
# 连续获得龙王
dragon_king_days_continuous = int(temp[1])
dkdir[nickname] = {
"user_id": user_id,
"days_max": dragon_king_days,
"days_contin": dragon_king_days_continuous,
}
# 瞎写的排序
dkdir_sorted = sorted(dkdir.items(), key=lambda d: d[1]["days_max"], reverse=True)
dkk = dkdir_sorted[0][1]["user_id"]
msg = UniMessage.text(f"本群获得龙王次数最多的是:\n{dkdir_sorted[0][0]}\n")
dk_avatar = f"http://q1.qlogo.cn/g?b=qq&nk={dkk}&s=5"
msg += UniMessage.image(url=dk_avatar)
msg += UniMessage.text("\n本群龙王榜\n")
for i, it in enumerate(dkdir_sorted[:5]):
_nickname = it[0]
_days_max = it[1]["days_max"]
_days_contin = it[1]["days_contin"]
msg += UniMessage.text(
f"第{i + 1}名:{_nickname},共{_days_max}次,最长连续{_days_contin}天获得龙王\n"
)
await msg.send()
# ============ 视频下载 ============
down_video = on_startswith("/video")
@down_video.handle()
async def down_video_handle(bot: Bot, ev: GroupMessageEvent):
url = ev.message.extract_plain_text().removeprefix("/video").strip()
if not url:
return
match = re.match(r"https?:/{2}\w.+$", url)
if not match:
await UniMessage.text("参数不合法").send()
return
timespan = int(time.time())
filename = f"{timespan}"
file = TEMP_DIR / f"{filename}.mp4"
fileabs = str(file)
cover = TEMP_DIR / f"{filename}.mp4.jpg"
TEMP_DIR.mkdir(parents=True, exist_ok=True)
try:
info = await asyncio.to_thread(get_video_info, url)
match = re.search(r"MPEG-4 video(.*)\s*Size:(.*)MiB", info)
if not match:
await UniMessage.text("参数不正确").send()
return
info_type = match.group(1).strip()
if info_type != "(video/mp4)":
await UniMessage.text("未找到视频信息").send()
return
info_size = match.group(2).strip()
if not is_number(info_size):
await UniMessage.text("无法获取视频大小").send()
return
size = float(info_size)
if size >= 6.0:
await UniMessage.text("视频体积过大").send()
return
await UniMessage.text("正在发送...").send()
await asyncio.to_thread(download_video, url, TEMP_DIR, filename)
except Exception as e:
logger.error(e)
await UniMessage.text("获取视频失败了").send()
file.unlink(missing_ok=True)
cover.unlink(missing_ok=True)
return
try:
await local_video(f"file:///{fileabs}").send()
except Exception:
await UniMessage.text("发送失败了").send()
file.unlink(missing_ok=True)
cover.unlink(missing_ok=True)
# ============ 病毒消息拦截 ============
fuck_xxs = on_message(rule=is_type(GroupMessageEvent), block=False)
@fuck_xxs.handle()
async def fuck_xxs_handle(bot: Bot, ev: GroupMessageEvent):
for m in ev.message:
if m.type == "json" and '"app":"com.tencent.autoreply"' in m.data.get(
"data", ""
):
await bot.delete_msg(message_id=ev.message_id)
await UniMessage.text(
"这是onse病毒码,会获取到你手机的xxs,然后发起攻击...以下省略6行需要全文背诵的金句"
).send()
return
@@ -0,0 +1,3 @@
"""管理类触发(handlers/management)。"""
from . import admin, antivirus, ban, dragon, title, video # noqa: F401
@@ -0,0 +1,50 @@
"""群管理工具 —— 管理员与公告。"""
from __future__ import annotations
from nonebot import on_startswith, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot_plugin_alconna import UniMessage
from ...utils.permissions import check_perm
require("nonebot_plugin_alconna")
set_admin = on_startswith("设置管理员")
unset_admin = on_startswith("取消管理员")
set_group_notice = on_startswith("设置群公告")
@set_admin.handle()
async def set_admin_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_perm(bot, ev):
return
for seg in ev.message:
if seg.type == "at" and seg.data.get("qq") != "all":
await bot.set_group_admin(
group_id=ev.group_id, user_id=int(seg.data["qq"]), enable=True
)
await UniMessage.text("我好了").send()
@unset_admin.handle()
async def unset_admin_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_perm(bot, ev):
return
for seg in ev.message:
if seg.type == "at" and seg.data.get("qq") != "all":
await bot.set_group_admin(
group_id=ev.group_id, user_id=int(seg.data["qq"]), enable=False
)
await UniMessage.text("我好了").send()
@set_group_notice.handle()
async def set_group_notice_handle(bot: Bot, ev: GroupMessageEvent):
content = ev.message.extract_plain_text().removeprefix("设置群公告").strip()
if not content:
return
if not await check_perm(bot, ev):
return
await bot.call_api("_send_group_notice", group_id=ev.group_id, content=content)
await UniMessage.text("我好了").send()
@@ -0,0 +1,25 @@
"""群管理工具 —— 病毒消息拦截。"""
from __future__ import annotations
from nonebot import on_message, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot.rule import is_type
from nonebot_plugin_alconna import UniMessage
require("nonebot_plugin_alconna")
fuck_xxs = on_message(rule=is_type(GroupMessageEvent), block=False)
@fuck_xxs.handle()
async def fuck_xxs_handle(bot: Bot, ev: GroupMessageEvent):
for m in ev.message:
if m.type == "json" and '"app":"com.tencent.autoreply"' in m.data.get(
"data", ""
):
await bot.delete_msg(message_id=ev.message_id)
await UniMessage.text(
"这是onse病毒码,会获取到你手机的xxs,然后发起攻击...以下省略6行需要全文背诵的金句"
).send()
return
@@ -0,0 +1,145 @@
"""群管理工具 —— 禁言相关。"""
from __future__ import annotations
import math
import random
from nonebot import logger, on_fullmatch, on_message, on_regex, on_startswith, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent
from nonebot.rule import Rule, is_type
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from ...services.moderation import group_silence, member_silence, silence
from ...utils.message import get_at_id, has_at_all
from ...utils.permissions import check_perm
from ...utils.text import parse_seconds, strip_prefix
require("nonebot_plugin_alconna")
def _is_ban_cmd(event: GroupMessageEvent) -> bool:
text = event.message.extract_plain_text()
return text.startswith("禁言") and not text.startswith("禁言一下")
ban_user = on_message(rule=Rule(_is_ban_cmd) & is_type(GroupMessageEvent), block=True)
ban_member = on_startswith(("来发口球", "塞口球", "禁言一下"))
one_for_one = on_startswith("极限一换一")
cancel_ban = on_startswith(("解除口球", "取消口球", "摘口球", "脱口球", "取消禁言", "解除禁言", "解禁"))
ban_all = on_fullmatch(("全员口球", "全员禁言"))
cancel_ban_all = on_fullmatch(("取消全员口球", "取消全员禁言", "解除全员口球", "解除全员禁言"))
sleep_8h = on_fullmatch(("睡眠套餐", "休眠套餐", "精致睡眠", "来一份精致睡眠套餐"))
tea = on_regex(r"(来|來)(.*(份|个)(.*)(睡|茶)(.*))套餐")
@ban_user.handle()
async def ban_user_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_perm(bot, ev, silent=True):
return
msg = ev.message.extract_plain_text().removeprefix("禁言").strip()
at_seg = next(
(seg for seg in ev.message if seg.type == "at" and seg.data.get("qq") != "all"),
None,
)
if not at_seg:
return
try:
uid = int(at_seg.data["qq"])
minutes = 1 if not msg else int(msg)
await bot.set_group_ban(group_id=ev.group_id, user_id=uid, duration=minutes * 60)
await UniMessage.text("我好了").send()
except Exception as ex:
logger.error(ex)
@ban_member.handle()
async def ban_member_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
duration_text = strip_prefix(
ev.message.extract_plain_text(), ("来发口球", "塞口球", "禁言一下")
)
if not duration_text:
duration_text = "30"
if has_at_all(ev):
await group_silence(bot, ev, gid, True)
return
sid = get_at_id(ev) or uid
await member_silence(bot, ev, uid, sid, gid, duration_text, silent=False)
@one_for_one.handle()
async def one_for_one_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
duration_text = strip_prefix(ev.message.extract_plain_text(), ("极限一换一",))
if has_at_all(ev):
await UniMessage.text("人干事?").send(at_sender=True)
await member_silence(bot, ev, uid, uid, gid, "180", silent=False)
return
seconds = parse_seconds(duration_text)
if seconds is None:
await UniMessage.text("请输入正确时长~").send(at_sender=True)
return
if seconds > 30:
await UniMessage.text("时长不可大于30秒哟~").send(at_sender=True)
return
sid = get_at_id(ev) or uid
await member_silence(bot, ev, uid, sid, gid, str(seconds), silent=True)
await member_silence(bot, ev, uid, uid, gid, str(seconds), silent=True)
await (
UniMessage.at(user_id=str(uid))
.text("成功一换一")
.at(user_id=str(sid))
.text(f"{seconds}秒~")
.send()
)
@cancel_ban.handle()
async def cancel_ban_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
uid = ev.user_id
gid = ev.group_id
if has_at_all(ev):
await group_silence(bot, ev, gid, False)
return
sid = get_at_id(ev)
if sid is None:
await UniMessage.text("请@需要摘口球的群员哦w").send()
return
await member_silence(bot, ev, uid, sid, gid, "0", silent=False)
@ban_all.handle()
async def ban_all_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
await group_silence(bot, ev, ev.group_id, True)
@cancel_ban_all.handle()
async def cancel_ban_all_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
await group_silence(bot, ev, ev.group_id, False)
@sleep_8h.handle()
async def sleep_8h_handle(bot: Bot, ev: MessageEvent):
await silence(bot, ev, 8 * 60 * 60)
@tea.handle()
async def tea_handle(bot: Bot, ev: MessageEvent):
base = 0 if "午" in ev.plain_text else 5 * 60 * 60
length = len(ev.plain_text)
sleep_time = base + round(math.sqrt(length) * 60 * 30 + 60 * random.randint(-15, 15))
await silence(bot, ev, sleep_time)
@@ -0,0 +1,84 @@
"""群管理工具 —— 龙王。"""
from __future__ import annotations
import random
import re
from nonebot import on_fullmatch, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot_plugin_alconna import UniMessage
from ...constants import IMG_DIR
from ...utils.media import image_to_base64
from ...utils.permissions import honor_info, self_member_info
require("nonebot_plugin_alconna")
whois_dragon = on_fullmatch(("谁是龙王", "迫害龙王", "龙王是谁"))
dragon_list = on_fullmatch(("龙王榜", "龙王排行", "龙王排行榜", "龙王榜单"))
@whois_dragon.handle()
async def whois_dragon_handle(bot: Bot, ev: GroupMessageEvent):
gid = ev.group_id
self_info = await self_member_info(bot, gid)
sid = self_info["user_id"]
ta_info = await honor_info(bot, gid, "talkative")
if not ta_info or not ta_info.get("current_talkative"):
await UniMessage.text("本群没有开启龙王标志哦~").send()
return
dk = ta_info["current_talkative"]["user_id"]
if sid == dk:
pic = image_to_base64(IMG_DIR / "dk_is_me.jpg")
await (
UniMessage.text("你们这群丢人玩意,龙王怎么又是我\n")
.image(url=pic)
.send()
)
else:
action = random.choice(["龙王出来挨透", "龙王出来喷水"])
dk_avatar = ta_info["current_talkative"]["avatar"] + "640" + f"&t={dk}"
await (
UniMessage.at(user_id=str(dk))
.text(f"\n{action}\n")
.image(url=dk_avatar)
.send()
)
@dragon_list.handle()
async def dragon_list_handle(bot: Bot, ev: GroupMessageEvent):
gid = ev.group_id
ta_info = await honor_info(bot, gid, "talkative")
if not ta_info or not ta_info.get("talkative_list"):
await UniMessage.text("本群暂时没有龙王数据哦~").send()
return
dragon_king_list_info = ta_info["talkative_list"]
dkdir = {}
for item in dragon_king_list_info:
user_id = item["user_id"]
nickname = item["nickname"]
description = item["description"]
temp = re.findall(r"\d+", description)
dragon_king_days = int(temp[0])
dragon_king_days_continuous = int(temp[1])
dkdir[nickname] = {
"user_id": user_id,
"days_max": dragon_king_days,
"days_contin": dragon_king_days_continuous,
}
dkdir_sorted = sorted(dkdir.items(), key=lambda d: d[1]["days_max"], reverse=True)
dkk = dkdir_sorted[0][1]["user_id"]
msg = UniMessage.text(f"本群获得龙王次数最多的是:\n{dkdir_sorted[0][0]}\n")
dk_avatar = f"http://q1.qlogo.cn/g?b=qq&nk={dkk}&s=5"
msg += UniMessage.image(url=dk_avatar)
msg += UniMessage.text("\n本群龙王榜\n")
for i, it in enumerate(dkdir_sorted[:5]):
_nickname = it[0]
_days_max = it[1]["days_max"]
_days_contin = it[1]["days_contin"]
msg += UniMessage.text(
f"第{i + 1}名:{_nickname},共{_days_max}次,最长连续{_days_contin}天获得龙王\n"
)
await msg.send()
@@ -0,0 +1,58 @@
"""群管理工具 —— 头衔 / 名片 / 群名。"""
from __future__ import annotations
from nonebot import on_fullmatch, on_startswith, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot_plugin_alconna import UniMessage
from hexi.plugins.nonebot_plugin_hexi_core.custom_utils import check_manage
from ...services.profile import card_edit, group_name, title_get
from ...utils.message import get_at_id
from ...utils.text import strip_prefix
require("nonebot_plugin_alconna")
special_title = on_startswith("申请头衔")
del_special_title = on_fullmatch(("删除头衔", "清除头衔", "收回头衔", "回收头衔"))
card_set = on_startswith(("修改名片", "修改群名片", "设置名片", "设置群名片"))
set_group_name = on_startswith(("修改群名", "设置群名"))
@special_title.handle()
async def special_title_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
title = strip_prefix(ev.message.extract_plain_text(), ("申请头衔",))
sid = get_at_id(ev) or uid
await title_get(bot, ev, uid, sid, gid, title)
@del_special_title.handle()
async def del_special_title_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
sid = get_at_id(ev) or uid
await title_get(bot, ev, uid, sid, gid, None)
@card_set.handle()
async def card_set_handle(bot: Bot, ev: GroupMessageEvent):
uid = ev.user_id
gid = ev.group_id
card_text = strip_prefix(
ev.message.extract_plain_text(), ("修改名片", "修改群名片", "设置名片", "设置群名片")
)
sid = get_at_id(ev) or uid
await card_edit(bot, ev, uid, sid, gid, card_text)
@set_group_name.handle()
async def set_group_name_handle(bot: Bot, ev: GroupMessageEvent):
if not await check_manage(bot, ev):
await UniMessage.text("权限不足").send()
return
gid = ev.group_id
name_text = strip_prefix(ev.message.extract_plain_text(), ("修改群名", "设置群名"))
await group_name(bot, ev, gid, name_text)
@@ -0,0 +1,71 @@
"""群管理工具 —— 视频下载。"""
from __future__ import annotations
import asyncio
import re
import time
from nonebot import logger, on_startswith, require
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
from nonebot_plugin_alconna import UniMessage
from ...constants import TEMP_DIR
from ...services.video import download_video, get_video_info, local_video
from ...utils.text import is_number
require("nonebot_plugin_alconna")
down_video = on_startswith("/video")
@down_video.handle()
async def down_video_handle(bot: Bot, ev: GroupMessageEvent):
url = ev.message.extract_plain_text().removeprefix("/video").strip()
if not url:
return
match = re.match(r"https?:/{2}w.+$", url)
if not match:
await UniMessage.text("参数不合法").send()
return
timespan = int(time.time())
filename = f"{timespan}"
file = TEMP_DIR / f"{filename}.mp4"
fileabs = str(file)
cover = TEMP_DIR / f"{filename}.mp4.jpg"
TEMP_DIR.mkdir(parents=True, exist_ok=True)
try:
info = await asyncio.to_thread(get_video_info, url)
match = re.search(r"MPEG-4 video(.*)s*Size:(.*)MiB", info)
if not match:
await UniMessage.text("参数不正确").send()
return
info_type = match.group(1).strip()
if info_type != "(video/mp4)":
await UniMessage.text("未找到视频信息").send()
return
info_size = match.group(2).strip()
if not is_number(info_size):
await UniMessage.text("无法获取视频大小").send()
return
size = float(info_size)
if size >= 6.0:
await UniMessage.text("视频体积过大").send()
return
await UniMessage.text("正在发送...").send()
await asyncio.to_thread(download_video, url, TEMP_DIR, filename)
except Exception as e:
logger.error(e)
await UniMessage.text("获取视频失败了").send()
file.unlink(missing_ok=True)
cover.unlink(missing_ok=True)
return
try:
await local_video(f"file:///{fileabs}").send()
except Exception:
await UniMessage.text("发送失败了").send()
file.unlink(missing_ok=True)
cover.unlink(missing_ok=True)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Some files were not shown because too many files have changed in this diff Show More