- 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>
400 lines
14 KiB
Python
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
|