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>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""群管理工具插件的通用工具子包。"""
|
||||
@@ -0,0 +1,27 @@
|
||||
import base64
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def image_to_base64(file_path: Path) -> str | None:
|
||||
"""图片转 base64,文件不存在时返回 None"""
|
||||
if not file_path.is_file():
|
||||
return None
|
||||
with open(file_path, "rb") as f:
|
||||
return "base64://" + base64.b64encode(f.read()).decode()
|
||||
|
||||
|
||||
def audio_to_base64(file_path: Path) -> str:
|
||||
"""音频转 base64"""
|
||||
with open(file_path, "rb") as f:
|
||||
return "base64://" + base64.b64encode(f.read()).decode()
|
||||
|
||||
|
||||
def random_audio(dir_path: Path) -> str | None:
|
||||
"""随机取目录下的一个语音文件并转 base64,目录不存在或为空时返回 None"""
|
||||
if not dir_path.is_dir():
|
||||
return None
|
||||
files = [p for p in dir_path.iterdir() if p.is_file()]
|
||||
if not files:
|
||||
return None
|
||||
return audio_to_base64(random.choice(files))
|
||||
@@ -0,0 +1,13 @@
|
||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent
|
||||
|
||||
|
||||
def get_at_id(ev: GroupMessageEvent) -> int | None:
|
||||
"""提取消息中第一个被 at 的用户(排除 at all)"""
|
||||
for m in ev.message:
|
||||
if m.type == "at" and m.data.get("qq") != "all":
|
||||
return int(m.data["qq"])
|
||||
return None
|
||||
|
||||
|
||||
def has_at_all(ev: GroupMessageEvent) -> bool:
|
||||
return any(m.type == "at" and m.data.get("qq") == "all" for m in ev.message)
|
||||
@@ -0,0 +1,49 @@
|
||||
from nonebot import logger
|
||||
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_bot_admin,
|
||||
check_manage,
|
||||
)
|
||||
|
||||
|
||||
async def group_member_info(bot: Bot, gid: int, uid: int):
|
||||
"""获取指定群成员信息,失败返回 None"""
|
||||
try:
|
||||
return await bot.get_group_member_info(group_id=gid, user_id=uid, no_cache=True)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return None
|
||||
|
||||
|
||||
async def self_member_info(bot: Bot, gid: int):
|
||||
"""获取 bot 自身的群成员信息"""
|
||||
return await group_member_info(bot, gid, bot.self_id)
|
||||
|
||||
|
||||
async def honor_info(bot: Bot, gid: int, honor_type: str):
|
||||
"""获取群荣誉信息,失败返回 None"""
|
||||
try:
|
||||
return await bot.get_group_honor_info(group_id=gid, type=honor_type)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return None
|
||||
|
||||
|
||||
def is_bot_manage(self_info) -> bool:
|
||||
"""bot 是否为该群的管理/群主"""
|
||||
return bool(self_info) and self_info["role"] in ("owner", "admin")
|
||||
|
||||
|
||||
async def check_perm(bot: Bot, ev: GroupMessageEvent, silent: bool = False) -> bool:
|
||||
"""检查 bot 为群管理 且 用户为管理以上权限;不通过时按需回复"""
|
||||
if not await check_bot_admin(bot, ev.group_id):
|
||||
await UniMessage.text("咱又不是管理,你找错人了").send()
|
||||
return False
|
||||
if not await check_manage(bot, ev):
|
||||
if not silent:
|
||||
await UniMessage.text("才不听你的呢").send()
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,37 @@
|
||||
import unicodedata
|
||||
|
||||
|
||||
def parse_seconds(s: str) -> int | None:
|
||||
"""解析 '30' 或 '3*60' 形式的时长秒数,非法输入返回 None"""
|
||||
s = s.strip()
|
||||
if not s or not all(c.isdigit() or c == "*" for c in s):
|
||||
return None
|
||||
try:
|
||||
result = 1
|
||||
for part in s.split("*"):
|
||||
result *= int(part)
|
||||
return result
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def strip_prefix(text: str, prefixes) -> str:
|
||||
"""依次移除前缀后去空白"""
|
||||
for p in prefixes:
|
||||
text = text.removeprefix(p)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def is_number(s) -> bool:
|
||||
"""判断字符串是否可视为数字"""
|
||||
try:
|
||||
float(s)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
unicodedata.numeric(s)
|
||||
return True
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return False
|
||||
Reference in New Issue
Block a user