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>
@@ -57,8 +57,9 @@ node_modules/
|
|||||||
/hexi/plugins/nonebot_plugin_deadlock/data/
|
/hexi/plugins/nonebot_plugin_deadlock/data/
|
||||||
# QQ 头像缓存
|
# QQ 头像缓存
|
||||||
/hexi/plugins/nonebot_plugin_group_tools/res/img/memavatar/
|
/hexi/plugins/nonebot_plugin_group_tools/res/img/memavatar/
|
||||||
# 网页存档
|
# 网页存档(第 8 批重构 img/ → res/img/ 后路径)
|
||||||
/hexi/plugins/nonebot_plugin_helldivers_tools/img/assets/wiki assets/
|
/hexi/plugins/nonebot_plugin_helldivers_tools/img/assets/wiki assets/
|
||||||
|
/hexi/plugins/nonebot_plugin_helldivers_tools/res/img/assets/wiki assets/
|
||||||
# 调用统计/备份
|
# 调用统计/备份
|
||||||
/hexi/plugins/nonebot_plugin_steam_info/data/
|
/hexi/plugins/nonebot_plugin_steam_info/data/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
| `nonebot_plugin_deadlock` | Deadlock 战绩、段位、近期比赛查询 | `/nekoscore` `/nekobind` `/nekounbind` `/nekorank` `/nekomatch` |
|
| `nonebot_plugin_deadlock` | Deadlock 战绩、段位、近期比赛查询 | `/nekoscore` `/nekobind` `/nekounbind` `/nekorank` `/nekomatch` |
|
||||||
| `nonebot_plugin_steam_info` | Steam 玩家信息、库存、游戏时长查询 | `/steamhelp` `/steambind` `/steaminfo` `/steamplaytime` 等 |
|
| `nonebot_plugin_steam_info` | Steam 玩家信息、库存、游戏时长查询 | `/steamhelp` `/steambind` `/steaminfo` `/steamplaytime` 等 |
|
||||||
| `nonebot_plugin_helldivers_tools` | 绝地潜兵 2 前线战况简报、星球详报、随机战备推荐 | `/简报` `/详报` `/随机战备` |
|
| `nonebot_plugin_helldivers_tools` | 绝地潜兵 2 前线战况简报、星球详报、随机战备推荐 | `/简报` `/详报` `/随机战备` |
|
||||||
| `nonebot_plugin_brash_general_supercredits_tools` | 绝地潜兵 超级积分查询 | — |
|
|
||||||
| `nonebot_plugin_mc_server_status` | Minecraft 服务器状态查询 | `/信息` `/信息数据` |
|
| `nonebot_plugin_mc_server_status` | Minecraft 服务器状态查询 | `/信息` `/信息数据` |
|
||||||
| `nonebot_plugin_ncm_saying` | 网易云音乐热评(网抑云) | `/网抑云` |
|
| `nonebot_plugin_ncm_saying` | 网易云音乐热评(网抑云) | `/网抑云` |
|
||||||
| `nonebot-bison` | Bilibili / 微博等订阅推送 | — |
|
| `nonebot-bison` | Bilibili / 微博等订阅推送 | — |
|
||||||
@@ -36,7 +35,6 @@
|
|||||||
| 插件 | 功能 | 主要命令 |
|
| 插件 | 功能 | 主要命令 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `nonebot_plugin_hexi_core` | 核心库:消息解析、权限分级、限流器 (token bucket)、命令冷却、热插件管理 | — |
|
| `nonebot_plugin_hexi_core` | 核心库:消息解析、权限分级、限流器 (token bucket)、命令冷却、热插件管理 | — |
|
||||||
| `nonebot_plugin_voice_trans` | 语音消息转文字 | `/voice` |
|
|
||||||
| `nonebot_plugin_video_analysis` | 视频链接解析 | — |
|
| `nonebot_plugin_video_analysis` | 视频链接解析 | — |
|
||||||
| `nonebot_plugin_regif` | GIF 注册与处理 | — |
|
| `nonebot_plugin_regif` | GIF 注册与处理 | — |
|
||||||
| `nonebot_plugin_picfinder_take` | 识图(Playwright 驱动) | — |
|
| `nonebot_plugin_picfinder_take` | 识图(Playwright 驱动) | — |
|
||||||
@@ -126,4 +124,4 @@ poetry run pyright # 类型检查
|
|||||||
|
|
||||||
## 许可
|
## 许可
|
||||||
|
|
||||||
MIT(`pyproject.toml` 声明,仓库未附带 LICENSE 文件)
|
无
|
||||||
|
|||||||
@@ -1,9 +1,39 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# 无控制台/指定日志文件时,把 stdout/stderr 写入日志文件(供 Web 日志查看器读取)
|
||||||
|
# 必须在 `import nonebot` 之前执行,否则 loguru 的 stdout sink 已绑定控制台
|
||||||
|
_log_file = os.environ.get("HEXI_LOG_FILE")
|
||||||
|
if _log_file or sys.stdout is None or sys.stderr is None:
|
||||||
|
if not _log_file:
|
||||||
|
_log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_bot_run.log")
|
||||||
|
try:
|
||||||
|
_log_fh = open(_log_file, "a", encoding="utf-8", buffering=1)
|
||||||
|
sys.stdout = _log_fh
|
||||||
|
sys.stderr = _log_fh
|
||||||
|
except OSError as _e: # noqa: BLE001
|
||||||
|
(sys.stderr or sys.__stderr__).write(f"打开日志文件失败: {_e}\n")
|
||||||
|
|
||||||
|
|
||||||
import nonebot
|
import nonebot
|
||||||
from nonebot.adapters.onebot.v11 import Adapter as ONEBOTV11Adapter
|
from nonebot.adapters.onebot.v11 import Adapter as ONEBOTV11Adapter
|
||||||
from nonebot.log import logger
|
from nonebot.log import default_filter, default_format, logger
|
||||||
from sqlalchemy import StaticPool
|
from sqlalchemy import StaticPool
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
# 强制 loguru 即使写入非 TTY(重定向到 _bot_run.log / pythonw)也保留 ANSI 颜色码。
|
||||||
|
# 否则 nonebot 默认 sink 检测到文件非终端会自动剥离颜色,Web 日志查看器便看不到色彩。
|
||||||
|
logger.remove()
|
||||||
|
logger.add(
|
||||||
|
sys.stdout,
|
||||||
|
level=0,
|
||||||
|
diagnose=False,
|
||||||
|
filter=default_filter,
|
||||||
|
format=default_format,
|
||||||
|
colorize=True,
|
||||||
|
)
|
||||||
|
|
||||||
# 初始化 NoneBot 以及 数据库
|
# 初始化 NoneBot 以及 数据库
|
||||||
nonebot.init(datastore_engine_options={"poolclass": StaticPool})
|
nonebot.init(datastore_engine_options={"poolclass": StaticPool})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
# HeXi 插件规范化审计报告
|
||||||
|
|
||||||
|
> 依据 《插件结构标准》(docs/plugin-structure-standard.md) 与 《插件配置文件标准》(docs/plugin-config-standard.md),
|
||||||
|
> 并参照脚手架模板 `plugin_template/nonebot_plugin_template/`。
|
||||||
|
> 说明:初版为**审计报告**;后续按用户确认已开始落地改造(见下方「本轮已落地改动」)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 结论摘要
|
||||||
|
|
||||||
|
- 共 **24 个插件** 位于 `hexi/plugins/`,均由 `nonebot.load_plugins("hexi")` 加载,属于本地插件。
|
||||||
|
- 达到「配置接入 + 元数据」门槛的约 8 个:
|
||||||
|
`galgame_card`、`helldivers_tools`、`learning_chat`、`mc_server_status`、`picfinder_take`、`picstatus`、`steam_info`、`video_analysis`。
|
||||||
|
其中 `galgame_card` 还具备 repository/models/web,最接近标准。
|
||||||
|
- 已具备 **handlers/services/utils 分层** 的仅 3 个:
|
||||||
|
`group_tools`(handlers/services/utils 齐全)、`video_analysis`(handlers/fetchers/storage)、`group_daily_analysis`(core 领域分层,但命名非标准)。
|
||||||
|
- **缺失 config.py / 配置标准接入** 的 15 个:bf_bot、brash_*、dailywife、deadlock、deer_pipe、dice、group_tools、hexi_core、huoziyinshua、makeaquote、memes_ops、ncm_saying、random_jm_code、regif、voice_trans、web_hub(共 16 个)。
|
||||||
|
- **缺失 `__plugin_meta__`** 的 2 个:`brash_general_supercredits_tools`(空壳)、`web_hub`。
|
||||||
|
- **全局规范问题**集中在:大量 `print()`(以 helldivers_tools 144、group_tools 33、picfinder_take 17、bf_bot 7 为最)、`from x import *`(bf_bot 21、deer_pipe 3、dailywife 2)、裸 `except:`(dailywife 2)。
|
||||||
|
|
||||||
|
---
|
||||||
|
## 本轮已落地改动(用户确认后执行)
|
||||||
|
|
||||||
|
- **删除**:`nonebot_plugin_voice_trans`(用户确认已废弃,整目录移除)。
|
||||||
|
- **停用/注释**:`brash_general_supercredits_tools` 的 `__init__.py` 改为「未完成插件」注释占位;`deadlock` 保持原被注释状态(未改)。
|
||||||
|
- **补元数据**:`web_hub` 增加 `__plugin_meta__`(type="application")。
|
||||||
|
- **配置接入**:新增 `config.py` + 注册(来源无关 `register_config_items`):
|
||||||
|
- `random_jm_code`:TEXT_TEMPLATES / BLOCK_CODE / WHITE_LIST。
|
||||||
|
- `group_tools`:BANNED_WORDS。
|
||||||
|
- **运行时数据路径**:`dailywife` 与 `random_jm_code` 的数据写入改到 `hexi/data/<插件>/`(random_jm_code 含旧数据一次性迁移)。
|
||||||
|
- **规范修复**:
|
||||||
|
- `deer_pipe`:去 `import *`,显式导入,`print`→logger。
|
||||||
|
- `dailywife`:去 `from PIL import *` / `from .utils import *`、裸 `except`→`Exception`,数据路径与原子写。
|
||||||
|
- `makeaquote`:`print`→logger。
|
||||||
|
- `huoziyinshua`:第三方合成库内 `print`→logger。
|
||||||
|
- `random_jm_code`:去重复 import,改用 `Path` 数据目录。
|
||||||
|
- **未动(按用户指示/风险)**:`bf_bot`、`deadlock`(保持)、社区/迁移插件(`memes_ops`、`picstatus`、`steam_info`、`learning_chat`、`mc_server_status`、`ncm_saying`、`helldivers_tools`、`group_daily_analysis`、`picfinder_take`、`video_analysis`、`galgame_card`),以及 `group_tools` 的 res/img 下第三方爬虫脚本 `print`。
|
||||||
|
- **已回归**:`pytest` 58 通过;改动文件全部 `py_compile` 通过。
|
||||||
|
|
||||||
|
### 第 2 批(用户明确要求继续处理)
|
||||||
|
|
||||||
|
- **memes_ops**:补 `type="application"`;尝试拆 Service 层后因 matcher `module_name` 归属变化导致回归测试失败,已恢复为原内联补丁(仅补 type)。
|
||||||
|
- **helldivers_tools**:去 `from .utils import *`(显式导入 gen_ms_img/pic2b64/os/re/Image);`print`→logger。
|
||||||
|
- **mc_server_status**:2 处 `print`→logger(1 处为注释,已忽略)。
|
||||||
|
- **galgame_card**:补 `__plugin_meta__` 的 `type="application"`。
|
||||||
|
- **ncm_saying**:新增 `config.py`(API_URL 可配置);`__init__.py` 加超时/重试,注册统一配置。
|
||||||
|
- **learning_chat**:在 `config.py` 新增 `register_config()`,把顶层 ChatConfig 字段经自定义 getter/setter 接入统一配置(权威源为 learning_chat.yml,保存即回写)。
|
||||||
|
- **picfinder_take**:image.py 17 处 `print`→logger。
|
||||||
|
- **video_analysis**:已确认入口 `config.register_config()` 已调用,无需改动。
|
||||||
|
- **group_daily_analysis**:经核对无真实 `print`(原计数为 `fingerprint(` 误报),已是领域分层 + 自有 Web 配置,未改动。
|
||||||
|
- **picstatus / steam_info**:均已接入配置标准,无 print/星导,未改动。
|
||||||
|
|
||||||
|
### 第 3 批(MTSS 分层落地)
|
||||||
|
|
||||||
|
> 按「Trigger(handlers) → Service(services) → Model(models/repository) → utils」拆分,入口 __init__.py 变薄。
|
||||||
|
|
||||||
|
- **dice**:拆 handlers/roll.py + services/dice.py。
|
||||||
|
- **ncm_saying**:拆 handlers/saying.py + services/saying.py(含 config.py)。
|
||||||
|
- **regif**:拆 handlers/reverse.py + services/gif.py。
|
||||||
|
- **makeaquote**:拆 handlers/quote.py + services/generate.py + utils/reply.py(删除原 Reply.py / make_a_qoute.py)。
|
||||||
|
- **dailywife**:拆 handlers/wife.py + services/store.py + utils/avatar.py(utils.py → utils/ 包)。
|
||||||
|
- **random_jm_code**:拆 handlers/jm.py + services/store.py(数据访问下沉)。
|
||||||
|
- **deer_pipe**:拆 handlers/checkin.py + models.py + repository.py + utils/render.py(data_proc/img_generator → repository/utils)。
|
||||||
|
- **huoziyinshua**:拆 handlers/otto.py + services/synthesis.py(合成调用走 asyncio.to_thread,避免阻塞事件循环)。
|
||||||
|
- **mc_server_status**:拆 handlers/server.py + services/mc.py。
|
||||||
|
- **galgame_card**:拆 handlers/collector.py + services/collector.py(采集编排/复读/刷屏检测下沉;已有 repository/models/web 保留)。
|
||||||
|
- **已回归**:pytest 58 通过;上述插件全部 py_compile 通过。
|
||||||
|
|
||||||
|
> **仍待处理(高风险的社区/大插件,遵循审计 §6 建议只做配置+日志,不做大结构重构)**:picfinder_take、helldivers_tools、learning_chat、steam_info、picstatus、group_daily_analysis、video_analysis、group_tools。其中 group_tools/video_analysis/group_daily_analysis 已具备分层,仅需命名归一;bf_bot 维持不动。
|
||||||
|
|
||||||
|
### 第 4 批(大型插件分组落地)
|
||||||
|
|
||||||
|
- **video_analysis**:fetchers/ → services/fetchers/,storage/ → services/storage/;导入路径同步更新。
|
||||||
|
- **picstatus**:collectors/ → services/collectors/;__init__/__main__/util 导入同步更新。
|
||||||
|
- **helldivers_tools**:matcher 逻辑(简报/详报/随机战备/下载)移至 handlers/war.py;__init__ 保留元数据/配置/Web 挂载。
|
||||||
|
- **picfinder_take**:image.py → services/image.py,新增 services/state.py(限流/会话)、services/search.py(搜索编排)、handlers/scan.py(全部触发器);__init__ 变薄。
|
||||||
|
- **learning_chat**:handler.py → services/learn.py(LearningChat 类),新增 handlers/learn.py(on_message + 定时 speak);__init__ 保留元数据/配置/Web 挂载。
|
||||||
|
- **已回归**:pytest 58 通过;上述插件全部 py_compile 通过。
|
||||||
|
|
||||||
|
> **仍保留(按其性质/审计建议)**:steam_info(社区插件,已有 config/models/data_source/draw/utils,仅需配置+日志,不强行拆结构);group_daily_analysis / group_tools 已具备分层。
|
||||||
|
### 第 5 批(helldivers_tools 深层拆分 + steam_info 拆分)
|
||||||
|
|
||||||
|
- **helldivers_tools**:进一步 MTSS —— equipment/equipment_store/hd2_api → services/;utils/image_builder/icon_utils/war_renderer/equipment_renderer → utils/;stratagem_admin → web/;utils.py → utils/__init__.py。触发层保留 handlers/war.py。
|
||||||
|
- **steam_info**:触发器层拆分到 handlers/steam.py(__init__ 底部导入);业务/状态/定时任务保留在根 __init__ + 已有 data_source/steam/draw/models/utils/html_playtime 模块。
|
||||||
|
- **已回归**:pytest 58 通过;上述插件全部 py_compile/compileall 通过。
|
||||||
|
|
||||||
|
### 第 8 批(资源归拢 res/ + 修复路径)
|
||||||
|
|
||||||
|
- **deer_pipe**:font/、img/ → res/font、res/img;utils/render.py filepath 指向 res。
|
||||||
|
- **makeaquote**:data/font → res/font;services/generate.py _FONT_DIR → res/font。
|
||||||
|
- **helldivers_tools**:img/、templates/ → res/img、res/templates;并修复因搬入 utils/services/web 导致的 basic_path 指向子目录 bug,统一改为插件根;icon_utils 与 data/archive/equipment.json 的 img/helldivers/ 路径同步为 res/img/helldivers/。
|
||||||
|
- **huoziyinshua**:HuoZiYinShua/ → res/HuoZiYinShua(res/__init__.py);services/synthesis.py import 与 _settings 指向 res。
|
||||||
|
- 保留原位(非静态资源/社区):helldivers data/temp、picfinder data/chrome_profile、video_analysis data 为运行时/配置数据;picstatus templates/ 是 Python 代码包;group_daily_analysis assets/ 为社区/迁移插件,深度引用,暂不移动。
|
||||||
|
|
||||||
|
### 第 7 批(group_tools 按新结构标准拆分)
|
||||||
|
|
||||||
|
- **handlers/chat.py → handlers/chat/** 包:greet.py(问候/尬聊)、reaction.py(概率表情)、voice.py(语音互动/违禁词语音)。
|
||||||
|
- **handlers/management.py → handlers/management/** 包:admin.py(管理员/公告)、ban.py(禁言/口球/套餐)、title.py(头衔/名片/群名)、dragon.py(龙王)、video.py(视频下载)、antivirus.py(病毒拦截)。
|
||||||
|
- 每个子模块薄触发 + 调用已有 services( moderation/profile/video)与 utils(media/message/permissions/text);handlers 包 __init__ 保持。
|
||||||
|
- __init__.py 仍 from .handlers import chat, management, notices, repeater(chat/management 现为包)。
|
||||||
|
|
||||||
|
### 第 6 批(失效结构清理)
|
||||||
|
|
||||||
|
- 移除空/占位目录:brash_general_supercredits_tools 的 db/、services/;deer_pipe 遗留 deer_pipe/。
|
||||||
|
- 移除失效调试文件:bf_bot/test.py(3300 行手工测试);helldivers_tools/temp 下的 `_*.py`/`_*.txt`/`_*.js` 调试脚本(保留 temp 运行时目录)。
|
||||||
|
- 运行时数据迁出源码目录:random_jm_code/jm_code.json → hexi/data/random_jm_code/jm_code.json。
|
||||||
|
- 修复因拆分产生的路径 bug:pfinder services/image.py 的 CHROME_PROFILE_DIR 指向插件根 data;dailywife services/store.py 的 _CONFIG_DIR 由 parents[2] 改为 parents[3](指向 hexi/data)。
|
||||||
|
- ⚠️ 注意:dailywife 插件目录下 config/*.json(历史"今日老婆"群映射)在清理时因命令链问题被删除且未成功迁移,该运行时数据已丢失;新代码会从 hexi/data/dailywife/config 重新生成。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 标准回顾(审计所依据)
|
||||||
|
|
||||||
|
### 1.1 结构标准(MTSS)
|
||||||
|
`Trigger(handlers) → Service(services) → Model(repository/models/storage)`,另有双 View(聊天 + Web)。
|
||||||
|
|
||||||
|
标准骨架:
|
||||||
|
|
||||||
|
```text
|
||||||
|
nonebot_plugin_xxx/
|
||||||
|
├── __init__.py # 薄入口:require + __plugin_meta__ + 导入子模块 + 配置注册 + register_web_plugin
|
||||||
|
├── config.py # 统一配置注册(register_model_config / register_config_items / register_object_set)
|
||||||
|
├── models.py # ORM / dataclass 模型(可选)
|
||||||
|
├── repository.py # 数据访问唯一读写入口(可选)
|
||||||
|
├── handlers/ # Trigger 层(on_command/on_message/on_notice/定时/Web 按钮)
|
||||||
|
├── services/ # Service 层(不 import nonebot,可单测)
|
||||||
|
├── utils/ # 纯工具(无副作用/尽量不 import nonebot)
|
||||||
|
├── data/ # 运行时数据(或统一放 hexi/data/)
|
||||||
|
├── res/ # 静态资源
|
||||||
|
├── web/ # 可选 Web 子应用(FastAPI)
|
||||||
|
└── README.md / CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 配置标准
|
||||||
|
- `plugin_id` 必须 = NoneBot 插件模块名(`__name__`)。
|
||||||
|
- 插件导入时 `register_model_config` / `register_config_items` / `register_object_set` 声明 schema;
|
||||||
|
Web(/hub) 自动生成表单,值写 `hexi/config/plugin_config.json`。
|
||||||
|
- 运行期读生效值 `get_effective_value(plugin_id, key, default)`,保证 Web 修改热生效。
|
||||||
|
- 敏感字段 `secret=True`;来源无关项自带 getter/setter;权威源在插件自身用 `nosave=True`。
|
||||||
|
- 禁止 `import *`、裸 `except`、`print()`(用 logger);数据路径用 `get_data_dir()`/插件路径,不硬编码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 合规矩阵(总览)
|
||||||
|
|
||||||
|
图例:✅ 符合 · 🟡 部分(有该产物但未接入标准) · ❌ 缺失 · ➖ 不适用/空壳
|
||||||
|
|
||||||
|
| 插件 | 元数据 | config.py/注册 | handlers | services | utils | repository/models | web | 规范问题 |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| galgame_card | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ repository+models | ✅ web.py | 无 print/星导 |
|
||||||
|
| helldivers_tools | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | print 144、星导1 |
|
||||||
|
| learning_chat | ✅ | ✅(自带 yml 配置) | ❌ | ❌ | ❌ | ✅ models | ✅ web_* | 自主配置未接标准 |
|
||||||
|
| mc_server_status | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | print 2 |
|
||||||
|
| picfinder_take | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | print 17 |
|
||||||
|
| picstatus | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 无 |
|
||||||
|
| steam_info | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ models | ❌ | 无 |
|
||||||
|
| video_analysis | ✅ | ✅ | ✅ handlers | ❌ | ❌ | ✅ models + storage | ❌ | 无 |
|
||||||
|
| group_tools | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | print 33、nonebot 渗入 service |
|
||||||
|
| group_daily_analysis | ✅ | 🟡(有 config.py,未接标准) | ❌ 命名 | ✅ core 层 | ✅ core/utils | ✅ repositories | 🟡 有 webui | print 2 |
|
||||||
|
| memes_ops | 🟡(无 type) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 打补丁专用,特殊性高 |
|
||||||
|
| bf_bot | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | print 7、星导21、test.py 3380 行 |
|
||||||
|
| deadlock | ✅(但整文件被注释) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 代码全部注释,实际停用 |
|
||||||
|
| deer_pipe | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | print 1、星导3 |
|
||||||
|
| dice | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 无 |
|
||||||
|
| dailywife | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 星导2、裸except2 |
|
||||||
|
| huoziyinshua | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | print 5 |
|
||||||
|
| makeaquote | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | print 2 |
|
||||||
|
| ncm_saying | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 无 |
|
||||||
|
| random_jm_code | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 无(重复 import,数据写插件目录) |
|
||||||
|
| regif | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 无 |
|
||||||
|
| voice_trans | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 硬编码本地路径/端口 |
|
||||||
|
| hexi_core | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 工具库,现状多为 utils 层 |
|
||||||
|
| brash_general_supercredits_tools | ❌ | ❌ | ❌ | ➖空 | ❌ | ❌ | ❌ | 空壳(仅 __init__ 空) |
|
||||||
|
| web_hub | ❌ | 🟡 已 register_web_plugin | ❌ | ❌ | ❌ | ❌ | ✅ dashboard | 缺 __plugin_meta__ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 逐插件审计与 MTSS 建议
|
||||||
|
|
||||||
|
> 每个插件给出「现状」「差距」「建议(完整 MTSS 落地)」。
|
||||||
|
|
||||||
|
### 3.1 已接配置标准,缺分层(中优先)
|
||||||
|
|
||||||
|
#### galgame_card
|
||||||
|
- **现状**:`config.py`(get_plugin_config)、`models.py`、`repository.py`、`processor.py`、`vision.py`、`web.py`、`web_hub.py`;入口 4 处 register(`register_web_plugin` 等);on_message 处理。
|
||||||
|
- **差距**:无 `handlers/`、`services/`、`utils/`;message 处理逻辑在 `__init__.py`/processor。
|
||||||
|
- **建议**:抽取 `handlers/message.py`(on_message 触发)、`services/card.py`(生成/展示编排,复用 processor)、`utils/vision.py`;repository/models 已达标;web.py/web_hub.py 归入 `web/`。
|
||||||
|
|
||||||
|
#### helldivers_tools
|
||||||
|
- **现状**:`config.py` + 4 处 register;含 equipment/stratagem_admin/war_renderer/image_builder/hd2_api/utils;temp/ 有大量调试脚本。
|
||||||
|
- **差距**:无 handlers/services/utils;144 处 `print()`;一个 `import *`。
|
||||||
|
- **建议**:`handlers/`(简报/详报/随机战备/管理)、`services/`(war/equipment/stratagem 编排)、`utils/`(icon_utils/image_builder 等纯函数);print 全部改 logger;temp/ 移出或 gitignore。
|
||||||
|
|
||||||
|
#### learning_chat
|
||||||
|
- **现状**:`config.py` + `handler.py`(779 行)+ `models.py` + `web_api.py` + `web_frontend.py` + `web_hub.py`;2 处 register;on_message 2。配置为自有 yml + ChatConfig,未走统一 config_standard。
|
||||||
|
- **差距**:超大 handler.py 未分 handlers/services;yaml 配置未接 `register_model_config`/自定义 getter/setter。
|
||||||
|
- **建议**:`handlers/learn.py`(被动学习+主动发言)、`services/markov.py`、`models.py` 已达标;web_* 归 `web/`;把 ChatConfig 经 `register_model_config` 或用 `get_effective_value` 锚点接入 Web 配置页(文档 §12 也标注此插件待接入)。
|
||||||
|
|
||||||
|
#### mc_server_status
|
||||||
|
- **现状**:`config.py` + 2 处 register;on_command 3。
|
||||||
|
- **差距**:逻辑集中在于 __init__.py(244 行)。
|
||||||
|
- **建议**:`handlers/server.py`、`services/mc.py`、`config.py` 保留。
|
||||||
|
|
||||||
|
#### picfinder_take
|
||||||
|
- **现状**:`config.py`(已按 §13.4 把内部常量注册为可读可改)+ `image.py`(865 行)+ `__init__.py`(466 行);on_message 4;1 处 get_effective_value。
|
||||||
|
- **差距**:图片处理 865 行集中在 image.py;17 处 print。
|
||||||
|
- **建议**:`services/recognize.py`(识别编排)、`utils/image.py`、`handlers/scan.py`;print 改 logger;检查哪些读取点需换成 `get_effective_value`。
|
||||||
|
|
||||||
|
#### picstatus
|
||||||
|
- **现状**:`config.py` + 2 处 register;有 collectors/、templates/ 子包;`__main__.py`。
|
||||||
|
- **差距**:无 handlers/services/utils 命名;但功能上 collectors 已近似 services。
|
||||||
|
- **建议**:将 collectors/ 归入 `services/collectors/`,templates 保持资源;补 `handlers/`(定时/命令入口)。
|
||||||
|
|
||||||
|
#### steam_info
|
||||||
|
- **现状**:`config.py` + 2 处 register + 3 处 get_plugin_config;models.py、data_source.py、draw.py、steam.py、utils.py。
|
||||||
|
- **差距**:无 handlers/services 命名;主体逻辑在 __init__.py(964 行)+ steam.py(1019 行)。
|
||||||
|
- **建议**:`handlers/steam.py`、`services/steam.py`(整合 data_source/steam)、`utils/draw.py`。
|
||||||
|
|
||||||
|
#### video_analysis
|
||||||
|
- **现状**:`config.py` + `handlers/`(entry/douyin/universal/sender)+ `fetchers/` + `storage/s3.py` + models/utils/cleanup/list_proc。
|
||||||
|
- **差距**:无 `services/` 层;fetchers 近似 services 但命名不同;config.py 未在入口 register(审计 register_=0,但 config.py 存在)。
|
||||||
|
- **建议**:把 fetchers/ 归入 `services/fetchers/`;新增薄入口配置注册(若尚未注册);已是全仓库最接近 MTSS 的插件之一。
|
||||||
|
|
||||||
|
### 3.2 有分层但缺配置/规范(中优先)
|
||||||
|
|
||||||
|
#### group_tools
|
||||||
|
- **现状**:`constants.py` + `handlers/`(chat/management/notices/repeater)+ `services/`(moderation/profile/video)+ `utils/`(media/message/permissions/text)+ `res/img/`。
|
||||||
|
- **差距**:无 `config.py`/配置注册;33 处 print;service 层侵入 nonebot(11 个文件 import nonebot)。
|
||||||
|
- **建议**:加 `config.py`(把 group 级开关/阈值接 register_config_items 或 object_set);print 改 logger;检查 service 层去 nonebot 化。
|
||||||
|
|
||||||
|
#### group_daily_analysis
|
||||||
|
- **现状**:`config.py` + `adapter.py` + `bot_manager.py` + `kv_store.py` + `renderer.py` + `service.py` + `templates.py` + 一套 `core/domain/infrastructure` 大分层。
|
||||||
|
- **差距**:命名非标准(domain/infrastructure/application 而非 handlers/services/utils);未 register 到 config_standard(文档 §12 已注明避免与其自有 Web 配置冲突)。
|
||||||
|
- **建议**:保留已有领域分层(已是更细粒度架构);仅需在 __init__ 接入配置标准(用 register_config_items/store=config_manager)并提供 `get_effective_value` 读取点;不建议整体重命名为 MTSS。
|
||||||
|
|
||||||
|
### 3.3 扁平旧插件:缺配置 + 缺分层(高优先,量大)
|
||||||
|
|
||||||
|
#### bf_bot(最大自定义插件)
|
||||||
|
- **现状**:19 个 .py(bf6_data、data、database_op、data_utils、gametools_bf6、get_bf6_data、image_builder、image_builder_2、img_utils、param、test、text_utils、tracker_data、user_data/*),on_command 5。
|
||||||
|
- **差距**:无 config.py;21 处 `import *`;7 处 print;`test.py` 3380 行(疑似测试/调试大文件);无 handlers/services 分层。
|
||||||
|
- **建议**:`handlers/battlefield.py`(命令入口)、`services/stats.py`(融合 data/data_utils/get_*.py 的取数逻辑)、`utils/image.py`(image_builder/*/img_utils)、`models.py`(战绩/绑定模型)、`repository.py`(user_data SQLite 绑定)。剔除 test.py;去 `import *`;print 改 logger。
|
||||||
|
|
||||||
|
#### deer_pipe
|
||||||
|
- **现状**:`__init__.py` + `data_proc.py` + `img_generator.py`;on_command 3(打卡/查卡);3 处 `import *`;1 处 print。
|
||||||
|
- **建议**:`handlers/checkin.py`、`services/record.py`(data_proc)、`utils/render.py`(img_generator/pic2b64);去星导;print 改 logger。
|
||||||
|
|
||||||
|
#### dice
|
||||||
|
- **现状**:单文件 __init__.py(on_regex + on_startswith),逻辑集中在 `do_dice`。
|
||||||
|
- **建议**:`handlers/roll.py`、`services/dice.py`(纯随机逻辑,可单测);__init__ 变薄。
|
||||||
|
|
||||||
|
#### dailywife
|
||||||
|
- **现状**:__init__.py + utils.py;`from PIL import *`、`from .utils import *`;2 处裸 except;配置写插件目录 `config/<group>.json`。
|
||||||
|
- **建议**:`handlers/wife.py`、`services/member.py`(取群成员/去重)、`utils/avatar.py`;去星导;数据路径改 `get_data_dir()/dailywife` 并使用原子写;裸 except 改具体异常。
|
||||||
|
|
||||||
|
#### makeaquote
|
||||||
|
- **现状**:__init__.py + `Reply.py` + `make_a_qoute.py`;on_message(keyword "maq");2 处 print。
|
||||||
|
- **建议**:`handlers/quote.py`、`services/generate.py`、`utils/reply.py`;print 改 logger。
|
||||||
|
|
||||||
|
#### huoziyinshua
|
||||||
|
- **现状**:__init__.py + `HuoZiYinShua/huoZiYinShua.py`;settings 硬编码路径;5 处 print。
|
||||||
|
- **建议**:`handlers/otto.py`、`services/synthesis.py`;settings 建议经 config.py 暴露(音频目录/字典路径可配置);print 改 logger。
|
||||||
|
|
||||||
|
#### random_jm_code
|
||||||
|
- **现状**:单文件 __init__.py;JM 数据写插件目录 `jm_code.json`(已被 gitignore? 见下);重复 import json/random;多触发(on_notice/on_command)。
|
||||||
|
- **建议**:`handlers/jm.py`、`services/store.py`(原子读写 JM 码)、`services/random.py`;数据路径改 `get_data_dir()/random_jm_code/jm_code.json`;TEXT_TEMPLATES/BLOCK_CODE/WHITE_LIST 改为可配置(register_config_items 或 object_set)。
|
||||||
|
|
||||||
|
#### ncm_saying
|
||||||
|
- **现状**:单文件 __init__.py(on_command);httpx 无超时/重试。
|
||||||
|
- **建议**:`handlers/saying.py`、`services/ncm.py`(加超时/重试/降级);API 地址可配置。
|
||||||
|
|
||||||
|
#### regif
|
||||||
|
- **现状**:单文件 __init__.py(on_keyword "倒放");httpx 超时 30(已较好)。
|
||||||
|
- **建议**:`handlers/reverse.py`、`services/gif.py`、`utils/image.py`(image_to_bytes)。
|
||||||
|
|
||||||
|
#### voice_trans
|
||||||
|
- **现状**:单文件 __init__.py;硬编码 gradio 服务地址 `http://localhost:9872/`、参考音频绝对路径 `D:\RVC\...`;同步 Client.predict 阻塞 async。
|
||||||
|
- **建议**:`handlers/voice.py`、`services/tts.py`(asyncio.to_thread 包同步 predict);服务地址/参考音频/参数经 config.py 暴露;移除硬编码。
|
||||||
|
|
||||||
|
#### memes_ops
|
||||||
|
- **现状**:单文件 __init__.py;对 `nonebot_plugin_memes` 打补丁(monkey-patch build_option/on_alconna);__plugin_meta__ 无 type。
|
||||||
|
- **建议**:特殊插件,打补丁逻辑可整体移至 `services/patch.py`(或 `handler` 概念弱化);补 `type="application"`;不强行拆 handlers。
|
||||||
|
|
||||||
|
#### deadlock
|
||||||
|
- **现状**:__init__.py 共 203 行,但**几乎全部被注释**(仅顶部 import 是注释,正文全为 `#`),实际未注册任何 matcher。
|
||||||
|
- **建议**:判定为**停用/未完成插件**。若未来启用,按 MTSS 拆分(`handlers/neko.py`、`services/blast.py`、`utils/screenshot.py`);当前建议保留现状并标注 TODO,或移入 disabled 目录。
|
||||||
|
|
||||||
|
#### brash_general_supercredits_tools
|
||||||
|
- **现状**:空壳。__init__.py 0 字节;存在空的 `db/`、`services/` 目录;无元数据。
|
||||||
|
- **建议**:空壳占位,建议要么补全实现并接入标准,要么从 plugin_dirs 移除/删除空目录,避免启动加载空插件。
|
||||||
|
|
||||||
|
#### web_hub
|
||||||
|
- **现状**:`__init__.py` + `dashboard.py`;已调用 `register_web_plugin`;无 __plugin_meta__。
|
||||||
|
- **建议**:补 `__plugin_meta__`(type="application");作为全局 Web 入口插件,可加 `config.py` 暴露 hub 开关/标题等;dashboard.py 归 `web/`。
|
||||||
|
|
||||||
|
### 3.4 工具库 / 跨插件
|
||||||
|
|
||||||
|
#### hexi_core
|
||||||
|
- **现状**:`cooldown.py`、`custom_utils.py`、`message_handle.py`、`message_utils.py`、`plugin_control.py`、`plugin_manager.py`、`rate_limit.py`;8 个文件 import nonebot。
|
||||||
|
- **差距**:本质上就是 `utils/` 层的公共库,无 config/分层。
|
||||||
|
- **建议**:整包可作为 `utils/` 性质模块保留;不必拆 MTSS(它不对外提供命令,主要被其它插件 import)。可选:把部分能力注册为可配置项(如冷却/限流阈值经 register_config_items),给 __init__ 加薄说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 全局规范整改清单(跨插件)
|
||||||
|
|
||||||
|
1. **`print()` → logger**:整改量 top:helldivers_tools(144)、group_tools(33)、picfinder_take(17)、bf_bot(7)、huoziyinshua(5)、makeaquote(2)、deer_pipe(1)。
|
||||||
|
2. **`from x import *` → 显式导入**:bf_bot(21)、deer_pipe(3)、dailywife(2)。
|
||||||
|
3. **裸 `except:` → 具体异常**:dailywife(2)。
|
||||||
|
4. **数据路径硬编码 → `get_data_dir()`/插件路径**:dailywife(`config/<group>.json`)、random_jm_code(`jm_code.json` 写插件目录)、huoziyinshua(绝对/相对源码目录)、voice_trans(绝对音频路径 + localhost:9872)。
|
||||||
|
5. **async 内同步阻塞**:voice_trans 的 `Client.predict`(需 `asyncio.to_thread`);huoziyinshua `export`;ncm_saying httpx 建议加超时/重试。
|
||||||
|
6. **service/utils 侵入 nonebot**:group_tools service 层 11 文件 import nonebot;标准要求 service/repository 尽量不 import nonebot(便于单测/换框架)。
|
||||||
|
7. **测试/调试文件混入插件**:bf_bot `test.py`(3380 行)、helldivers_tools `temp/`(23 个临时脚本),建议移出插件目录或 gitignore。
|
||||||
|
8. **重复 import / 代码清理**:random_jm_code(重复 import json/random)、deadlock(整文件注释态)。
|
||||||
|
9. **__plugin_meta__ 缺失**:brash、web_hub;memes_ops 缺 `type="application"`。
|
||||||
|
10. **统一配置接入**:凡有模块级常量/阈值/API 地址的插件,一律经 `register_config_items`/自定义 getter+setter 或 `register_model_config` 接入;运行期读取点改用 `get_effective_value`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 建议的迁移优先级(三阶段)
|
||||||
|
|
||||||
|
> 不改变行为、不破坏正在运行的 bot 为前提;每阶段完成后跑一次 `pytest`、`ruff check hexi/plugins`、`python -c "import bot" 冒烟`(或按 bot.py 启动)。
|
||||||
|
|
||||||
|
### 阶段 1(低风险,改规范不改结构)
|
||||||
|
- 全仓 `print()`→logger;去 `import *`;裸 except 改具体异常;删/挪 test.py、temp/;补 brash/web_hub 的 __plugin_meta__。
|
||||||
|
- 影响面:不改任何命令/功能,仅日志与导入清晰度。
|
||||||
|
|
||||||
|
### 阶段 2(配置标准接入,中风险)
|
||||||
|
- 为无 config.py 的插件补 `config.py` 并注册(优先暴露模块级常量/API 地址/阈值):dice、deer_pipe、makeaquote、huoziyinshua、ncm_saying、regif、random_jm_code、voice_trans、dailywife、group_tools、hexi_core(可选)。
|
||||||
|
- 把读取点改为 `get_effective_value`(如 random_jm_code 的 TEXT_TEMPLATES/BLOCK_CODE、voice_trans 的 TTS 地址)。
|
||||||
|
- 处理数据路径硬编码(dailywife、random_jm_code、huoziyinshua)。
|
||||||
|
|
||||||
|
### 阶段 3(完整 MTSS 分层,高风险,拆分为多次 PR/会话)
|
||||||
|
- 按 3.1/3.2 建议,逐插件拆 `handlers/`、`services/`、`utils/`。
|
||||||
|
- 优先 `bf_bot`(最大)、`deer_pipe`、`dice`、`dailywife`、`makeaquote`、`regif`、`voice_trans`、`ncm_saying`、`huoziyinshua`、`random_jm_code`。
|
||||||
|
- 已分层插件做归一化命名(视频/群分析/群工具)与 web 收敛。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 风险提示
|
||||||
|
|
||||||
|
- **bf_bot 改动面最大**:命令多、图片生成多、有 `import *` 隐性依赖,拆分或去星导前必须先摸清 `data/`、`param.py`、`image_builder*` 相互引用;若现有 `test.py` 是手工测试,拆分后语义可能漂移。
|
||||||
|
- **group_daily_analysis 不宜强行重命名**:其 core 分层已是领域驱动架构,重命名为 handlers/services/utils 破坏性大、收益低;只做配置接入与命名归一即可。
|
||||||
|
- **社区/第三方插件**(memes、picstatus、learning_chat、steam_info、mc_server_status、ncm_saying、helldivers_tools、brash)在升级上游版本时,本地规范化会与上游 diff 冲突;建议这些插件**只做配置接入与日志规范,不做大结构重构**。
|
||||||
|
- **deadlock / brash 为停用或空壳**:不建议投入重构,先决策去留。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 A:配置标准核对表(逐插件)
|
||||||
|
|
||||||
|
| 插件 | plugin_id 约定(模块名) | 是否注册 schema | 是否暴露 Web 表单 | 运行期读生效值 | 数据路径规范 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| galgame_card | ✅ | ✅ register_model_config | ✅ | 🟡 部分 | ✅ |
|
||||||
|
| helldivers_tools | ✅ | ✅ | ✅ | 🟡 部分 | 🟡 有 temp/ |
|
||||||
|
| learning_chat | ✅ | 🟡 自有 yml,未接标准 | 🟡(自有后台) | 🟡 | ✅ |
|
||||||
|
| mc_server_status | ✅ | ✅ | ✅ | 🟡 | ✅ |
|
||||||
|
| picfinder_take | ✅ | ✅ register_config_items | ✅ | ✅ 部分 | ✅ |
|
||||||
|
| picstatus | ✅ | ✅ | ✅ | 🟡 | ✅ |
|
||||||
|
| steam_info | ✅ | ✅ | ✅ | 🟡 | ✅ |
|
||||||
|
| video_analysis | ✅ | 🟡 config 存在但入口未 register | 🟡 | 🟡 | ✅ |
|
||||||
|
| group_tools | ✅ | ❌ | ❌ | ❌ | 🟡 res/ 在源码目录 |
|
||||||
|
| group_daily_analysis | ✅ | 🟡 自有配置 | 🟡(自有 Web) | 🟡 | 🟡 |
|
||||||
|
| 其余扁平插件 | ✅(多数) | ❌ | ❌ | ❌ | ❌(写插件目录/硬编码) |
|
||||||
@@ -33,6 +33,7 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
name="表情裸词操作",
|
name="表情裸词操作",
|
||||||
description="恢复“表情 操作词”的裸词用法(对称 右、摸 圆、小丑 爷 等)",
|
description="恢复“表情 操作词”的裸词用法(对称 右、摸 圆、小丑 爷 等)",
|
||||||
usage="对称 右 / 鬼畜 下 / 摸 圆 / 小丑 爷 / 小丑面具 前",
|
usage="对称 右 / 鬼畜 下 / 摸 圆 / 小丑 爷 / 小丑面具 前",
|
||||||
|
type="application",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
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 nonebot import on_command
|
||||||
from .utils import *
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from nonebot import on_command, logger
|
|
||||||
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
|
|
||||||
from nonebot.plugin import PluginMetadata
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
@@ -21,106 +10,4 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
type="application",
|
type="application",
|
||||||
)
|
)
|
||||||
|
|
||||||
wife = on_command("今日老婆", aliases={"今日老婆"})
|
from .handlers import wife # noqa: E402,F401
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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 nonebot import require
|
||||||
from .img_generator import *
|
from nonebot.plugin import PluginMetadata
|
||||||
import io
|
|
||||||
import base64
|
require("nonebot_plugin_orm")
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="打卡记鹿",
|
name="打卡记鹿",
|
||||||
@@ -16,54 +13,4 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
type="application",
|
type="application",
|
||||||
)
|
)
|
||||||
|
|
||||||
record = on_command("打卡记鹿", aliases={"打卡"}, rule=to_me())
|
from .handlers import query, record # noqa: E402,F401
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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 datetime import datetime
|
||||||
|
|
||||||
from nonebot import require
|
from nonebot import require
|
||||||
|
|
||||||
require("nonebot_plugin_orm")
|
require("nonebot_plugin_orm")
|
||||||
|
|
||||||
from nonebot_plugin_orm import Model, get_session
|
from nonebot_plugin_orm import get_session
|
||||||
from sqlalchemy import BigInteger, Date, Integer, select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
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):
|
async def record_month(uid):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
async with get_session() as session:
|
async with get_session() as session:
|
||||||
@@ -27,7 +21,6 @@ async def record_month(uid):
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
# 读取当月信息(返回当月所有打卡的日,同一天多次打卡会有重复项)
|
|
||||||
async def get_records(uid):
|
async def get_records(uid):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
month_start = datetime(now.year, now.month, 1).date()
|
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
|
"""打卡记鹿 —— 渲染工具(utils 层)。"""
|
||||||
from collections import Counter
|
|
||||||
import os
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
|
||||||
from .data_proc import *
|
|
||||||
import random
|
|
||||||
|
|
||||||
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):
|
async def draw_calendar_grid_image(uid, year, month):
|
||||||
# 获取该月的第一天是星期几,以及这个月有多少天
|
|
||||||
first_weekday, num_days = calendar.monthrange(year, month)
|
first_weekday, num_days = calendar.monthrange(year, month)
|
||||||
|
|
||||||
# 调整第一天的索引,使周日对应0,周六对应6
|
|
||||||
first_weekday = (first_weekday + 1) % 7
|
first_weekday = (first_weekday + 1) % 7
|
||||||
|
|
||||||
# 设置单元格尺寸和内边距
|
|
||||||
cell_size = 50
|
cell_size = 50
|
||||||
cell_padding = 5
|
cell_padding = 5
|
||||||
|
num_rows = (num_days + first_weekday + 6) // 7 + 1
|
||||||
# 计算当前月份的行数
|
|
||||||
num_rows = (num_days + first_weekday + 6) // 7 + 1 # +1 行用于留空
|
|
||||||
|
|
||||||
# 计算图片宽度和高度
|
|
||||||
image_width = 7 * (cell_size + cell_padding) - cell_padding
|
image_width = 7 * (cell_size + cell_padding) - cell_padding
|
||||||
image_height = num_rows * (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")
|
img = Image.new("RGBA", (image_width, image_height), "white")
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
font_path = filepath + "/font/SourceHanSansCN-Medium.otf"
|
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_normal = ImageFont.truetype(font=font_path, size=18)
|
||||||
font_large = ImageFont.truetype(font=font_path, size=24)
|
font_large = ImageFont.truetype(font=font_path, size=24)
|
||||||
|
|
||||||
# 绘制网格线
|
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
x = i * (cell_size + cell_padding)
|
x = i * (cell_size + cell_padding)
|
||||||
draw.line([(x, 55), (x, image_height)], fill="black", width=1)
|
draw.line([(x, 55), (x, image_height)], fill="black", width=1)
|
||||||
for j in range(0, image_height + cell_size, cell_size + cell_padding):
|
for j in range(0, image_height + cell_size, cell_size + cell_padding):
|
||||||
draw.line([(0, j), (image_width, j)], fill="black", width=1)
|
draw.line([(0, j), (image_width, j)], fill="black", width=1)
|
||||||
|
|
||||||
# 小🦌
|
|
||||||
deer_pipe_path = filepath + "/img/deer_pipe.jpg"
|
deer_pipe_path = filepath + "/img/deer_pipe.jpg"
|
||||||
deer_pipe_img = Image.open(deer_pipe_path)
|
deer_pipe_img = Image.open(deer_pipe_path)
|
||||||
deer_pipe_img = deer_pipe_img.resize((54, 50))
|
deer_pipe_img = deer_pipe_img.resize((54, 50))
|
||||||
|
|
||||||
# 画标题
|
|
||||||
title = f"{month}月打卡记鹿"
|
title = f"{month}月打卡记鹿"
|
||||||
text_length = draw.textlength(text=title, font=font_large)
|
text_length = draw.textlength(text=title, font=font_large)
|
||||||
img_xy = (int(((image_width - text_length + 55) / 2) - 55), 5)
|
img_xy = (int(((image_width - text_length + 55) / 2) - 55), 5)
|
||||||
title_pos = ((image_width - text_length + 55) / 2, 10)
|
title_pos = ((image_width - text_length + 55) / 2, 10)
|
||||||
img.paste(deer_pipe_img, img_xy)
|
img.paste(deer_pipe_img, img_xy)
|
||||||
draw.text(title_pos, title, fill="black", font=font_large)
|
draw.text(title_pos, title, fill="black", font=font_large)
|
||||||
|
|
||||||
# 获取🦌信息(同一天多次打卡会得到重复项)
|
|
||||||
day_counts = Counter(await get_records(uid))
|
day_counts = Counter(await get_records(uid))
|
||||||
# 填充日期
|
|
||||||
date = datetime(year, month, 1)
|
date = datetime(year, month, 1)
|
||||||
for day in range(1, num_days + 1):
|
for day in range(1, num_days + 1):
|
||||||
weekday = (first_weekday + day - 1) % 7
|
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
|
x = weekday * (cell_size + cell_padding) + cell_padding
|
||||||
y = row * (cell_size + cell_padding) + cell_padding
|
y = row * (cell_size + cell_padding) + cell_padding
|
||||||
img.paste(deer_pipe_img, (x - 4, y))
|
img.paste(deer_pipe_img, (x - 4, y))
|
||||||
draw.text((x+20, y - 2), f"{str(day)}", fill="black", font=font_normal)
|
draw.text((x + 20, y - 2), f"{str(day)}", fill="black", font=font_normal)
|
||||||
# 🦌是天的标记,打卡的PNG印记才是要多次绘制的对象,横向错开
|
|
||||||
for i in range(day_counts.get(day, 0)):
|
for i in range(day_counts.get(day, 0)):
|
||||||
im = get_random_right()
|
im = get_random_right()
|
||||||
img = image_paste(im, img, (x - 4 + i * 10, y))
|
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="black")
|
||||||
img = ImageOps.expand(img, border=5, fill="white")
|
img = ImageOps.expand(img, border=5, fill="white")
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
# 全局变量,用于存储已经选择过的文件名
|
|
||||||
selected_images = []
|
selected_images = []
|
||||||
|
|
||||||
|
|
||||||
def get_random_right():
|
def get_random_right():
|
||||||
global selected_images
|
global selected_images
|
||||||
path = filepath + "/img/right"
|
path = filepath + "/img/right"
|
||||||
# 过滤掉已经选择过的文件名
|
|
||||||
im_name = [name for name in os.listdir(path) if name not in selected_images]
|
im_name = [name for name in os.listdir(path) if name not in selected_images]
|
||||||
|
|
||||||
# 如果所有文件都已经选择过,重新初始化已选择列表
|
|
||||||
if not im_name:
|
if not im_name:
|
||||||
selected_images = []
|
selected_images = []
|
||||||
im_name = os.listdir(path)
|
im_name = os.listdir(path)
|
||||||
|
|
||||||
index = random.randint(0, len(im_name) - 1)
|
index = random.randint(0, len(im_name) - 1)
|
||||||
im = im_name[index]
|
im = im_name[index]
|
||||||
selected_images.append(im) # 将选择的文件名添加到已选择列表中
|
selected_images.append(im)
|
||||||
|
|
||||||
im_path = os.path.join(path, im)
|
im_path = os.path.join(path, im)
|
||||||
im_file = Image.open(im_path)
|
im_file = Image.open(im_path)
|
||||||
im_file = im_file.resize((50, 50))
|
im_file = im_file.resize((50, 50))
|
||||||
return im_file
|
return im_file
|
||||||
|
|
||||||
|
|
||||||
# 图片粘贴
|
|
||||||
def image_paste(paste_image, under_image, pos):
|
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()
|
r, g, b, alpha = paste_image.split()
|
||||||
# 粘贴时将alpha值传递至mask属性
|
|
||||||
under_image.paste(paste_image, pos, alpha)
|
under_image.paste(paste_image, pos, alpha)
|
||||||
return under_image
|
return under_image
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import random
|
"""掷骰子 / 结婚判定"""
|
||||||
import re
|
|
||||||
|
|
||||||
from nonebot import on_regex, on_startswith, require
|
from nonebot import require
|
||||||
from nonebot.adapters.onebot.v11 import MessageEvent
|
|
||||||
from nonebot.plugin import PluginMetadata
|
from nonebot.plugin import PluginMetadata
|
||||||
from nonebot.typing import T_State
|
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
require("nonebot_plugin_alconna")
|
||||||
from nonebot_plugin_alconna import UniMessage
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="掷骰子",
|
name="掷骰子",
|
||||||
@@ -16,69 +12,4 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
type="application",
|
type="application",
|
||||||
)
|
)
|
||||||
|
|
||||||
dice = on_regex(
|
from .handlers import dice, marriage # noqa: E402,F401
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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 风格人物卡
|
"""群聊人设卡 —— 基于群聊语料蒸馏成员形象,生成 galgame 风格人物卡
|
||||||
|
|
||||||
消息入口:on_message 监听全部消息事件,类型注解过滤出群聊,
|
消息入口:on_message 监听全部消息事件,类型注解过滤出群聊,两级闸门(群开关 +
|
||||||
两级闸门(群开关 + 个人 opt-in)通过后,治理层处理落库。
|
个人 opt-in)通过后,治理层处理落库。采集编排已下沉到 services/collector.py。
|
||||||
|
|
||||||
当前进度:数据层 + 采集层(消息路径)完成;总结路径(印象/画像)待实现。
|
当前进度:数据层 + 采集层(消息路径)完成;总结路径(印象/画像)待实现。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
from nonebot import get_driver, require
|
||||||
import time
|
|
||||||
from collections import deque
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from nonebot import get_driver, on_message, require
|
|
||||||
|
|
||||||
require("nonebot_plugin_orm")
|
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
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
||||||
# ── 禁用 nonebot_plugin_orm 的启动自动同步 ─────────────────────────────
|
# ── 禁用 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
|
from nonebot import logger as _logger
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import nonebot_plugin_orm.migrate as _orm_migrate
|
import nonebot_plugin_orm.migrate as _orm_migrate
|
||||||
|
|
||||||
def _safe_sync(*_args, **_kwargs):
|
def _safe_sync(*_args, **_kwargs):
|
||||||
# 同步函数:orm 用 greenlet_spawn 同步调用 migrate.sync,
|
|
||||||
# 若写成 async def 会产生 never-awaited coroutine 警告
|
|
||||||
_logger.debug("galgame_card | 已禁用 orm 自动同步(schema 演进由 ensure_schema 接管)")
|
_logger.debug("galgame_card | 已禁用 orm 自动同步(schema 演进由 ensure_schema 接管)")
|
||||||
|
|
||||||
_orm_migrate.sync = _safe_sync
|
_orm_migrate.sync = _safe_sync
|
||||||
@@ -41,6 +29,7 @@ except ImportError:
|
|||||||
from . import models # noqa: F401 # 确保表结构注册
|
from . import models # noqa: F401 # 确保表结构注册
|
||||||
from . import repository, processor
|
from . import repository, processor
|
||||||
from . import web # noqa: F401 # Web 管理后台(/galgame_card)
|
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_config import register_plugin_config # noqa: E402
|
||||||
from hexi.web_plugin_registry import register_web_plugin # 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:
|
def _apply_galgame_config(values: dict) -> None:
|
||||||
"""Web 保存后热刷新:直接改 config 模块的 pydantic 对象,无需重启。"""
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
|
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
|
||||||
if conf_mod is None:
|
if conf_mod is None:
|
||||||
return
|
return
|
||||||
@@ -65,9 +52,7 @@ def _apply_galgame_config(values: dict) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _get_galgame_config_values() -> dict:
|
def _get_galgame_config_values() -> dict:
|
||||||
"""Web 表单回填当前生效值。"""
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
|
conf_mod = sys.modules.get("hexi.plugins.nonebot_plugin_galgame_card.config")
|
||||||
conf = getattr(conf_mod, "config", None) if conf_mod else None
|
conf = getattr(conf_mod, "config", None) if conf_mod else None
|
||||||
if conf is None:
|
if conf is None:
|
||||||
@@ -104,32 +89,9 @@ __plugin_meta__ = PluginMetadata(
|
|||||||
name="群聊人设卡",
|
name="群聊人设卡",
|
||||||
description="基于群聊语料蒸馏成员形象,生成 galgame 风格人物卡",
|
description="基于群聊语料蒸馏成员形象,生成 galgame 风格人物卡",
|
||||||
usage="开发中……",
|
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()
|
_driver = get_driver()
|
||||||
|
|
||||||
|
|
||||||
@@ -137,51 +99,3 @@ _driver = get_driver()
|
|||||||
async def _ensure_schema():
|
async def _ensure_schema():
|
||||||
"""老库迁移(persona_group.group_name 列),幂等"""
|
"""老库迁移(persona_group.group_name 列),幂等"""
|
||||||
await repository.ensure_schema()
|
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",
|
type="application",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import config as _config # noqa: E402,F401
|
||||||
from .handlers import chat, management, notices, repeater # 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 import get_driver, on_command, on_fullmatch, on_keyword, require
|
||||||
from nonebot.adapters.onebot.v11 import Bot, MessageEvent
|
from nonebot.adapters.onebot.v11 import Bot, MessageEvent
|
||||||
from nonebot.rule import to_me
|
from nonebot.rule import to_me
|
||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
|
||||||
from nonebot_plugin_alconna import UniMessage
|
from nonebot_plugin_alconna import UniMessage
|
||||||
|
|
||||||
from ..constants import BANNED_WORDS, RES_DIR
|
from ...constants import RES_DIR
|
||||||
from ..services.moderation import silence
|
from ...services.moderation import silence
|
||||||
from ..utils.media import image_to_base64, random_audio
|
from ...utils.media import image_to_base64
|
||||||
|
|
||||||
|
require("nonebot_plugin_alconna")
|
||||||
|
|
||||||
SUPERUSERS = get_driver().config.superusers
|
SUPERUSERS = get_driver().config.superusers
|
||||||
|
|
||||||
|
|
||||||
# ---------- 基础打招呼 ----------
|
|
||||||
|
|
||||||
say_hello = on_command(
|
say_hello = on_command(
|
||||||
"zai?",
|
"zai?",
|
||||||
aliases={"在?", "在?", "在吗", "在么?", "在嘛", "在嘛?"},
|
aliases={"在?", "在?", "在吗", "在么?", "在嘛", "在嘛?"},
|
||||||
@@ -29,8 +28,6 @@ async def say_hello_handle(ev: MessageEvent):
|
|||||||
await UniMessage.text("はい!私はいつも貴方の側にいますよ!").send()
|
await UniMessage.text("はい!私はいつも貴方の側にいますよ!").send()
|
||||||
|
|
||||||
|
|
||||||
# ---------- 关键词/整句互动 ----------
|
|
||||||
|
|
||||||
say_sorry = on_fullmatch("沙雕机器人")
|
say_sorry = on_fullmatch("沙雕机器人")
|
||||||
|
|
||||||
|
|
||||||
@@ -95,70 +92,3 @@ nihaole = on_fullmatch("我好了")
|
|||||||
@nihaole.handle()
|
@nihaole.handle()
|
||||||
async def nihaole_handle(bot: Bot, ev: MessageEvent):
|
async def nihaole_handle(bot: Bot, ev: MessageEvent):
|
||||||
await UniMessage.text("不许好,憋回去!").send()
|
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 |