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>
@@ -33,6 +33,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
name="表情裸词操作",
|
||||
description="恢复“表情 操作词”的裸词用法(对称 右、摸 圆、小丑 爷 等)",
|
||||
usage="对称 右 / 鬼畜 下 / 摸 圆 / 小丑 爷 / 小丑面具 前",
|
||||
type="application",
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -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
|
||||
@@ -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__],
|
||||
)
|
||||
|
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)
|
||||
|
Before Width: | Height: | Size: 923 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 76 KiB |