Files
HeXi/hexi/plugins/nonebot_plugin_group_tools/handlers/management.py
T
sansenhoshiandClaude b61d09f09f Add HeXi bot codebase: custom plugins, web frontends, tests
- hexi core: message handling, rate limiting, cooldown, plugin manager
- Custom plugins: BF stats, daily check-in, quotes, persona cards, etc.
- Community plugins vendored under hexi/plugins with local fixes
- Web admin frontends (learning-chat, persona-admin), unified hexi/web
- Tests for rate_limit/cooldown/memes/persona; poetry.lock

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 13:13:40 +08:00

400 lines
14 KiB
Python

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