diff --git a/Yunzai/.eslintrc.cjs b/Yunzai/.eslintrc.cjs index a9b525032da461f639576fefdc3551565af8f5d6..e052c260840c85dddc727698328d3900f2da890b 100644 --- a/Yunzai/.eslintrc.cjs +++ b/Yunzai/.eslintrc.cjs @@ -12,7 +12,9 @@ module.exports = { Bot: true, redis: true, logger: true, - plugin: true + plugin: true, + Renderer: true, + segment: true }, rules: { eqeqeq: ['off'], diff --git a/Yunzai/CHANGELOG.md b/Yunzai/CHANGELOG.md index d3c37215860902b012aa41e43fe4887801bc22ac..843a2417517d82f5b1e54c6de945fb57bbaac6cc 100644 --- a/Yunzai/CHANGELOG.md +++ b/Yunzai/CHANGELOG.md @@ -1,6 +1,6 @@ # 3.1.3 -* 支持协议端:QQBot +* 支持协议端:QQBot、go-cqhttp → OneBotv11、Lagrange * **请注意:** * 从3.1.3版本开始,原genshin包内的功能会逐步重构,与miao-plugin进行整合,以降低后续游戏版本升级时的维护成本 * 在整合过程中,可能会移除一些重复或迁移成本较高的功能,以及可能会有功能不稳定情况 diff --git a/Yunzai/README.md b/Yunzai/README.md index f397ff1f7b233bc86cb9395c45df0fc2b72be3a6..809963dc4c18ecca32a248c8306bd1e62c2b35cb 100644 --- a/Yunzai/README.md +++ b/Yunzai/README.md @@ -2,7 +2,7 @@ # TRSS-Yunzai -Yunzai 应用端,支持多账号,支持协议端:go-cqhttp、ComWeChat、GSUIDCore、ICQQ、QQBot、QQ频道、微信、KOOK、Telegram、Discord、OPQBot +Yunzai 应用端,支持多账号,支持协议端:OneBotv11、ComWeChat、GSUIDCore、ICQQ、QQBot、QQ频道、微信、KOOK、Telegram、Discord、OPQBot、Lagrange [![访问量](https://visitor-badge.glitch.me/badge?page_id=TimeRainStarSky.Yunzai&right_color=red&left_text=访%20问%20量)](https://github.com/TimeRainStarSky/Yunzai) [![Stars](https://img.shields.io/github/stars/TimeRainStarSky/Yunzai?color=yellow&label=收藏)](../../stargazers) @@ -13,7 +13,7 @@ Yunzai 应用端,支持多账号,支持协议端:go-cqhttp、ComWeChat、G -- 基于 [Miao-Yunzai](../../../../yoimiya-kokomi/Miao-Yunzai) 改造,需要同时安装 [miao-plugin](../../../../yoimiya-kokomi/miao-plugin) +- 基于 [Miao-Yunzai](../../../../yoimiya-kokomi/Miao-Yunzai) - 开发文档:[docs 分支](../../tree/docs) ## TRSS-Yunzai 后续计划 @@ -36,151 +36,179 @@ Yunzai 应用端,支持多账号,支持协议端:go-cqhttp、ComWeChat、G ### 手动安装 -> 环境准备: Windows or Linux,Node.js( [版本至少 v18 以上](http://nodejs.cn/download) ), [Redis](https://redis.io/docs/getting-started/installation) +> 环境准备:Windows/Linux/MacOS/Android +> [Node.js(>=v21)](https://nodejs.org), [Redis](https://redis.io), [Git](https://git-scm.com), [Chrome(可选)](https://google.cn/chrome) -1.克隆项目并安装 genshin miao-plugin TRSS-Plugin(可选) +1. Git Clone 项目 请根据网络情况选择使用 GitHub 或 Gitee 安装 -``` +```sh git clone --depth 1 https://github.com/TimeRainStarSky/Yunzai +git clone --depth 1 https://gitee.com/TimeRainStarSky/Yunzai cd Yunzai +``` + +2. 推荐安装插件(可选) + +```sh git clone --depth 1 https://github.com/TimeRainStarSky/Yunzai-genshin plugins/genshin git clone --depth 1 https://github.com/yoimiya-kokomi/miao-plugin plugins/miao-plugin git clone --depth 1 https://github.com/TimeRainStarSky/TRSS-Plugin plugins/TRSS-Plugin ``` -``` -git clone --depth 1 https://gitee.com/TimeRainStarSky/Yunzai -cd Yunzai +```sh git clone --depth 1 https://gitee.com/TimeRainStarSky/Yunzai-genshin plugins/genshin git clone --depth 1 https://gitee.com/yoimiya-kokomi/miao-plugin plugins/miao-plugin git clone --depth 1 https://Yunzai.TRSS.me plugins/TRSS-Plugin ``` -2.安装 [pnpm](https://pnpm.io/zh/installation) +3. 安装 [pnpm](https://pnpm.io/zh/installation) 和依赖 -``` -npm install -g pnpm -``` - -3.安装依赖 - -``` +```sh +npm i -g pnpm pnpm i ``` -4.运行 +4. 前台运行 -``` -node app -``` +| 操作 | 命令 | +| ---- | ---- | +| 启动 | node . | +| 停止 | node . stop | -5.启动协议端: +5. 启动协议端 -
go-cqhttp +
WebSocket
-下载运行 [go-cqhttp](https://docs.go-cqhttp.org),选择反向 WebSocket,修改 `config.yml`,以下为必改项: +
OneBotv11
-``` -uin: 账号 -password: '密码' -post-format: array -universal: ws://localhost:2536/go-cqhttp -``` +
go-cqhttp
-
+ 下载运行 [go-cqhttp](https://docs.go-cqhttp.org),选择反向 WebSocket,修改 `config.yml`,以下为必改项: -
ComWeChat + ```yaml + uin: 账号 + password: '密码' + post-format: array + universal: ws://localhost:2536/OneBotv11 + ``` -下载运行 [ComWeChat](https://justundertaker.github.io/ComWeChatBotClient),修改 `.env`,以下为必改项: +
-``` -websocekt_type = "Backward" -websocket_url = ["ws://localhost:2536/ComWeChat"] -``` +
LLOneBot
-
+ 下载安装 [LLOneBot](https://github.com/LLOneBot/LLOneBot),启用反向 WebSocket,添加地址: -
GSUIDCore + ``` + ws://localhost:2536/OneBotv11 + ``` -下载运行 [GenshinUID 插件](http://docs.gsuid.gbots.work/#/AdapterList),GSUIDCore 连接地址 修改为: +
-``` -ws://localhost:2536/GSUIDCore -``` +
Shamrock
-
+ 下载安装 [Shamrock](https://whitechi73.github.io/OpenShamrock),启用被动 WebSocket,添加地址: -
ICQQ + ``` + ws://localhost:2536/OneBotv11 + ``` -[TRSS-Yunzai ICQQ Plugin](../../../Yunzai-ICQQ-Plugin) +
-
- -
QQBot +
Lagrange
-[TRSS-Yunzai QQBot Plugin](../../../Yunzai-QQBot-Plugin) + 下载运行 [Lagrange.OneBot](https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot),修改 `appsettings.json` 中 `Implementations`: -
+ ```json + { + "Type": "ReverseWebSocket", + "Host": "localhost", + "Port": 2536, + "Suffix": "/OneBotv11", + "ReconnectInterval": 5000, + "HeartBeatInterval": 5000, + "AccessToken": "" + } + ``` -
QQ频道 +
-[TRSS-Yunzai QQGuild Plugin](../../../Yunzai-QQGuild-Plugin) +
- +
ComWeChat
-
微信 +下载运行 [ComWeChat](https://justundertaker.github.io/ComWeChatBotClient),修改 `.env`,以下为必改项: -[TRSS-Yunzai WeChat Plugin](../../../Yunzai-WeChat-Plugin) +```python +websocekt_type = "Backward" +websocket_url = ["ws://localhost:2536/ComWeChat"] +``` -
+
-
米游社大别野 +
GSUIDCore
-[TRSS-Yunzai mysVilla Plugin](../../../Yunzai-mysVilla-Plugin) +下载运行 [GenshinUID 插件](http://docs.gsuid.gbots.work/#/AdapterList),GSUIDCore 连接地址 修改为: -
+``` +ws://localhost:2536/GSUIDCore +``` -
KOOK +
-[TRSS-Yunzai KOOK Plugin](../../../Yunzai-KOOK-Plugin) +
OPQBot
-
+下载运行 [OPQBot](https://opqbot.com),启动参数添加: -
Telegram +``` +-wsserver ws://localhost:2536/OPQBot +``` -[TRSS-Yunzai Telegram Plugin](../../../Yunzai-Telegram-Plugin) +
-
+ -
Discord +
插件 -[TRSS-Yunzai Discord Plugin](../../../Yunzai-Discord-Plugin) +- [ICQQ](../../../Yunzai-ICQQ-Plugin) +- [QQBot](../../../Yunzai-QQBot-Plugin) +- [WeChat](../../../Yunzai-WeChat-Plugin) +- [KOOK](../../../Yunzai-KOOK-Plugin) +- [Telegram](../../../Yunzai-Telegram-Plugin) +- [Discord](../../../Yunzai-Discord-Plugin) +- [Route](../../../Yunzai-Route-Plugin) +- [Lagrange](../../../Yunzai-Lagrange-Plugin)
-
OPQBot - -下载运行 [OPQBot](https://opqbot.com),启动参数添加: +6. 设置主人:发送 `#设置主人`,日志获取验证码并发送 -``` --wsserver ws://localhost:2536/OPQBot -``` +7. 使用 [pm2](https://pm2.keymetrics.io) 后台运行 -
+| 操作 | 命令 | +| ---- | ---- | +| 启动 | pnpm start | +| 停止 | pnpm stop | +| 日志 | pnpm log | -
路由 +8. 开机自启 -[TRSS-Yunzai Route Plugin](../../../Yunzai-Route-Plugin) +```sh +pnpm start +pnpm pm2 save +pnpm pm2 startup +``` -
+## 班级群(¿ -6.设置主人:发送 `#设置主人`,后台日志获取验证码并发送 +1. [用户(897643592)](https://qm.qq.com/q/7NxbviGbj) +2. [开发者(833565573)](https://qm.qq.com/q/oFJR8VVECA) +3. [机器人(907431599)](https://qm.qq.com/q/oCBOrfE29U) ## 致谢 -| Nickname | Contribution | -| :-----------------------------------------------------------: | -------------------- | -| [Yunzai-Bot](../../../../Le-niao/Yunzai-Bot) | 乐神的 Yunzai-Bot | -| [Miao-Yunzai](../../../../yoimiya-kokomi/Miao-Yunzai) | 喵喵的 Miao-Yunzai | \ No newline at end of file +| Nickname | Contribution | +| -------- | ------------ | +| [Yunzai-Bot](../../../../Le-niao/Yunzai-Bot) | 乐神的 Yunzai-Bot | +| [Miao-Yunzai](../../../../yoimiya-kokomi/Miao-Yunzai) | 喵喵的 Miao-Yunzai | \ No newline at end of file diff --git a/Yunzai/app.js b/Yunzai/app.js index 6feed2aa2a5f229da43a369ed249523cfdfe5cfc..afe51337922f0b71a101ed2aafbaaccf7260fef7 100644 --- a/Yunzai/app.js +++ b/Yunzai/app.js @@ -1,3 +1,22 @@ -import Yunzai from "./lib/bot.js" -global.Bot = new Yunzai -Bot.run() \ No newline at end of file +switch (process.env.app_type || process.argv[2]) { + case "pm2": + case "start": { + global.Bot = new (await import("./lib/bot.js")).default + Bot.run() + break + } case "stop": { + const cfg = (await import("./lib/config/config.js")).default + const fetch = (await import("node-fetch")).default + try { + await fetch(`http://localhost:${cfg.bot.port}/exit`) + } catch (err) {} + process.exit() + } default: { + const { spawnSync } = await import("node:child_process") + while (!spawnSync(process.argv[0], + [process.argv[1], "start"], + { stdio: "inherit" }, + ).status) {} + process.exit() + } +} \ No newline at end of file diff --git a/Yunzai/config/default_config/bot.yaml b/Yunzai/config/default_config/bot.yaml index 86c6cf12f0f5539f444993fc96ed9433e3a10986..dd72c8ef8501efa09f53b6727328795a42773fc7 100644 --- a/Yunzai/config/default_config/bot.yaml +++ b/Yunzai/config/default_config/bot.yaml @@ -1,9 +1,30 @@ -# 日志等级:trace,debug,info,warn,fatal,mark,error,off -# mark时只显示执行命令,不显示聊天记录 +# 日志等级 trace,debug,info,warn,fatal,mark,error,off log_level: info +# 单条日志长度 +log_length: 10000 +# 对象日志格式 +log_object: true + +# 服务器地址 +url: http://localhost:2536 # 服务器端口 port: 2536 +# 自动更新时间 +update_time: 1440 +# 自动重启时间 +restart_time: 0 +# 上线推送通知的冷却时间 +online_msg_exp: 1440 +# 文件保存时间 +file_to_url_time: 1 +# 文件访问次数 +file_to_url_times: +# 消息类型统计 +msg_type_count: false +# 以/开头转为# +/→#: true + # chromium其他路径 chromium_path: # puppeteer接口地址 @@ -12,12 +33,4 @@ puppeteer_ws: puppeteer_timeout: # 米游社接口代理地址,国际服用 -proxyAddress: - -# 上线时给主人推送帮助 -online_msg: true -# 上线推送通知的冷却时间 -online_msg_exp: 86400 - -# 单条日志长度 -logLength: 1000 \ No newline at end of file +proxyAddress: \ No newline at end of file diff --git a/Yunzai/config/default_config/group.yaml b/Yunzai/config/default_config/group.yaml index f67819aaee9ca71f02f458898e06e52b1c5b0636..5a4fb1a7c6cfab99f54641cfd4d85a6d5cf1b014 100644 --- a/Yunzai/config/default_config/group.yaml +++ b/Yunzai/config/default_config/group.yaml @@ -3,7 +3,7 @@ default: groupCD: 500 # 群聊中所有指令操作冷却时间,单位毫秒,0则无限制 singleCD: 2000 # 群聊中个人操作冷却时间,单位毫秒 - onlyReplyAt: 0 # 是否只仅关注主动提及Bot的消息 0-否 1-是 + onlyReplyAt: 0 # 是否只仅关注主动提及Bot的消息 0-否 1-是 2-非主人 botAlias: # 开启后则只回复提及Bot的消息及特定前缀的消息 - 云崽 - 云宝 diff --git a/Yunzai/config/default_config/other.yaml b/Yunzai/config/default_config/other.yaml index 96810b376a6f70f070b6b8af25b7efe39ae0f603..69e19962c460b43cfaad57192468eef57e885a14 100644 --- a/Yunzai/config/default_config/other.yaml +++ b/Yunzai/config/default_config/other.yaml @@ -16,12 +16,14 @@ disableMsg: "私聊功能已禁用,仅支持发送cookie,抽卡记录链接 # 私聊通行字符串 disableAdopt: - stoken -#白名单群,配置后只在该群生效 -whiteGroup: +#白名单群 +whiteGroup: +#白名单用户 +whiteUser: #黑名单群 blackGroup: - 213938015 -#黑名单账号 -blackQQ: +#黑名单用户 +blackUser: - 528952540 \ No newline at end of file diff --git a/Yunzai/config/pm2.yaml b/Yunzai/config/pm2.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3622e75bf89a21d30b6c7173c981dfe41c66800c --- /dev/null +++ b/Yunzai/config/pm2.yaml @@ -0,0 +1,7 @@ +apps: + - name: TRSS-Yunzai + script: ./app.js + max_memory_restart: 512M + restart_delay: 60000 + env: + app_type: pm2 \ No newline at end of file diff --git a/Yunzai/lib/bot.js b/Yunzai/lib/bot.js index dba055ada3d6ecd751c6355406551048dcd1b2c0..ca19feb18b4a4e21ca5f658760e9ba4973962604 100644 --- a/Yunzai/lib/bot.js +++ b/Yunzai/lib/bot.js @@ -1,74 +1,103 @@ import "./config/init.js" import cfg from "./config/config.js" +import redisInit from "./config/redis.js" import PluginsLoader from "./plugins/loader.js" import ListenerLoader from "./listener/loader.js" import { EventEmitter } from "events" import express from "express" -import http from "http" +import http from "node:http" import { WebSocketServer } from "ws" import _ from "lodash" -import fs from "node:fs" +import fs from "node:fs/promises" +import path from "node:path" +import util from "node:util" import fetch from "node-fetch" import { randomUUID } from "node:crypto" -import { exec } from "child_process" +import { exec } from "node:child_process" import { fileTypeFromBuffer } from "file-type" import md5 from "md5" +import { Level } from "level" export default class Yunzai extends EventEmitter { constructor() { super() + this.stat = { start_time: Date.now()/1000 } this.uin = [] this.adapter = [] this.express = express() + for (const i of ["urlencoded", "json", "raw", "text"]) + this.express.use(express[i]({ extended: false })) + this.express.use(req => { + req.rid = `${req.ip}:${req.socket.remotePort}` + req.sid = `${req.protocol}://${req.hostname}:${req.socket.localPort}${req.originalUrl}` + this.makeLog("mark", ["HTTP", req.method, "请求", req.headers, req.query, req.body], `${req.sid} <= ${req.rid}`) + req.next() + }) + this.express.use("/exit", req => { + if (["::1", "::ffff:127.0.0.1"].includes(req.ip) || req.hostname == "localhost") + process.exit(1) + }) this.server = http.createServer(this.express) + this.server.on("error", err => { + if (typeof this[`server${err.code}`] == "function") + return this[`server${err.code}`](err) + this.makeLog("error", err, "Server") + }) this.server.on("upgrade", (...args) => this.wsConnect(...args)) this.wss = new WebSocketServer({ noServer: true }) - this.wsf = {} + this.wsf = Object.create(null) - this.fs = {} + this.fs = Object.create(null) this.express.use("/File", (...args) => this.fileSend(...args)) - this.fileToUrl("resources/http/File/404.jpg", { name: 404, time: 0 }) - this.fileToUrl("resources/http/File/timeout.jpg", { name: "timeout", time: 0 }) + for (const name of [404, "timeout"]) + this.fileToUrl(`resources/http/File/${name}.jpg`, { name, time: false, times: false }) } wsConnect(req, socket, head) { this.wss.handleUpgrade(req, socket, head, conn => { - conn.id = `${req.connection.remoteAddress}-${req.headers["sec-websocket-key"]}` - this.makeLog("mark", `${logger.blue(`[${conn.id} <=> ws://${req.headers.host}${req.url}]`)} 建立连接:${JSON.stringify(req.headers)}`) - conn.on("error", logger.error) - conn.on("close", () => this.makeLog("mark", `${logger.blue(`[${conn.id} <≠> ws://${req.headers.host}${req.url}]`)} 断开连接`)) - conn.on("message", msg => this.makeLog("debug", `${logger.blue(`[${conn.id} => ws://${req.headers.host}${req.url}]`)} 消息:${String(msg).trim()}`)) + conn.rid = `${req.socket.remoteAddress}:${req.socket.remotePort}-${req.headers["sec-websocket-key"]}` + conn.sid = `ws://${req.headers["x-forwarded-host"] || req.headers.host || `${req.socket.localAddress}:${req.socket.localPort}`}${req.url}` + this.makeLog("mark", ["建立连接", req.headers], `${conn.sid} <=> ${conn.rid}`) + conn.on("error", (...args) => this.makeLog("error", args, `${conn.sid} <=> ${conn.rid}`)) + conn.on("close", () => this.makeLog("mark", "断开连接", `${conn.sid} <≠> ${conn.rid}`)) + conn.on("message", msg => this.makeLog("debug", ["消息", this.String(msg)], `${conn.sid} <= ${conn.rid}`)) conn.sendMsg = msg => { if (!Buffer.isBuffer(msg)) msg = this.String(msg) - this.makeLog("debug", `${logger.blue(`[${conn.id} <= ws://${req.headers.host}${req.url}]`)} 消息:${msg}`) + this.makeLog("debug", ["消息", msg], `${conn.sid} => ${conn.rid}`) return conn.send(msg) } - for (const i of this.wsf[req.url.split("/")[1]] || []) + for (const i of this.wsf[req.url.split("/")[1]] || [() => conn.terminate()]) i(conn, req, socket, head) }) } - serverLoad() { - this.express.use(req => { - logger.mark(`${logger.blue(`[${req.ip} => http://${req.headers.host}${req.url}]`)} HTTP ${req.method} 请求:${JSON.stringify(req.headers)}`) - req.res.redirect("https://github.com/TimeRainStarSky/Yunzai") - }) + async serverEADDRINUSE(err) { + this.makeLog("error", ["监听端口", cfg.bot.port, "错误", err], "Server") + try { + await fetch(`http://localhost:${cfg.bot.port}/exit`) + } catch (err) {} + this.server_listen_time = (this.server_listen_time || 0) + 1 + await this.sleep(this.server_listen_time * 1000) + this.server.listen(cfg.bot.port) + } - this.server.listen(cfg.bot.port, () => { - logger.mark(`启动 HTTP 服务器:${logger.green(`http://[${this.server.address().address}]:${this.server.address().port}`)}`) - const url = cfg.bot.url.replace(/^http/, "ws") - for (const i of Object.keys(this.wsf)) - logger.info(`${i} 连接地址:${logger.blue(`${url}/${i}`)}`) - }) + async serverLoad() { + this.server.listen(cfg.bot.port) + await new Promise(resolve => this.server.once("listening", resolve)) + this.makeLog("mark", ["启动 HTTP 服务器", logger.green(`http://[${this.server.address().address}]:${this.server.address().port}`)], "Server") } async run() { + await redisInit() + await this.serverLoad() await import("./plugins/stdin.js") await PluginsLoader.load() await ListenerLoader.load() - this.serverLoad() + + this.express.use(req => req.res.redirect("https://github.com/TimeRainStarSky/Yunzai")) + this.makeLog("info", `连接地址:${logger.blue(`${cfg.bot.url.replace(/^http/, "ws")}/`)}${logger.cyan(`[${Object.keys(this.wsf)}]`)}`, "WebSocket") this.emit("online", this) } @@ -76,17 +105,195 @@ export default class Yunzai extends EventEmitter { return new Promise(resolve => setTimeout(resolve, time)) } + async fsStat(path) { try { + const stat = await fs.stat(path) + return stat + } catch (err) { + this.makeLog("trace", ["获取", path, "状态错误", err]) + return false + }} + + async mkdir(dir) { try { + if (await this.fsStat(dir)) return true + if (!await this.mkdir(path.dirname(dir))) return false + await fs.mkdir(dir) + return true + } catch (err) { + this.makeLog("error", ["创建", dir, "错误", err]) + return false + }} + + async rmdir(dir) { try { + if (!await this.fsStat(dir)) return true + for (const i of await fs.readdir(dir)) + await this.rm(`${dir}/${i}`) + await fs.rmdir(dir) + return true + } catch (err) { + this.makeLog("error", ["删除", dir, "错误", err]) + return false + }} + + async rm(file) { try { + const stat = await this.fsStat(file) + if (!stat) return true + if (stat.isDirectory()) + return this.rmdir(file) + await fs.unlink(file) + return true + } catch (err) { + this.makeLog("error", ["删除", file, "错误", err]) + return false + }} + + async download(url, file) { + let buffer + if (!file || (await this.fsStat(file))?.isDirectory?.()) { + const type = await this.fileType(url) + file = file ? path.join(file, type.name) : type.name + buffer = type.buffer + } else { + await this.mkdir(path.dirname(file)) + buffer = await this.Buffer(url) + } + await fs.writeFile(file, buffer) + return file + } + + makeMap(parent_map, parent_key, map) { + const save = async () => { try { + await parent_map.db.put(parent_key, { map_array: Array.from(map) }) + } catch (err) { + this.makeLog("error", ["写入", parent_map.db.location, parent_key, "错误", map, err]) + }} + + const set = map.set.bind(map) + Object.defineProperty(map, "set", { + value: async (key, value) => { + if (JSON.stringify(map.get(key)) != JSON.stringify(value)) { + set(key, value) + await save() + } + return map + }, + }) + const del = map.delete.bind(map) + Object.defineProperty(map, "delete", { + value: async key => { + if (!del(key)) return false + await save() + return true + }, + }) + return map + } + + async setMap(map, set, key, value) { + try { + if (value instanceof Map) { + set(key, this.makeMap(map, key, value)) + await map.db.put(key, { map_array: Array.from(value) }) + } else if (JSON.stringify(map.get(key)) != JSON.stringify(value)) { + set(key, value) + await map.db.put(key, value) + } + } catch (err) { + this.makeLog("error", ["写入", map.db.location, key, "错误", value, err]) + } + return map + } + + async delMap(map, del, key) { + if (!del(key)) return false + try { + await map.db.del(key) + } catch (err) { + this.makeLog("error", ["删除", map.db.location, key, "错误", err]) + } + return true + } + + async importMap(dir, map) { + for (const i of await fs.readdir(dir)) { + const path = `${dir}/${i}` + try { + await map.set(i, (await this.fsStat(path)).isDirectory() ? + await this.importMap(path, new Map) : + JSON.parse(await fs.readFile(path, "utf-8"))) + } catch (err) { + this.makeLog("error", ["读取", path, "错误", err]) + } + await this.rm(path) + } + await this.rm(dir) + return map + } + + async getMap(dir) { + const map = new Map() + const db = new Level(`${dir}-leveldb`, { valueEncoding: "json" }) + try { + await db.open() + for await (let [key, value] of db.iterator()) { + if (typeof value == "object" && value.map_array) + value = this.makeMap(map, key, new Map(value.map_array)) + map.set(key, value) + } + } catch (err) { + this.makeLog("error", ["打开", dir, "数据库错误", err]) + return map + } + + Object.defineProperty(map, "db", { value: db }) + const set = map.set.bind(map) + Object.defineProperty(map, "set", { + value: (key, value) => this.setMap(map, set, key, value), + }) + const del = map.delete.bind(map) + Object.defineProperty(map, "delete", { + value: key => this.delMap(map, del, key), + }) + + if (await this.fsStat(dir)) + await this.importMap(dir, map) + return map + } + String(data) { switch (typeof data) { case "string": return data case "object": - return JSON.stringify(data) + if (data instanceof Error) + return data.stack + if (Buffer.isBuffer(data)) + return String(data) } - return String(data) + return JSON.stringify(data) + } + + Loging(data) { + if (typeof data == "string") return data + if (!cfg.bot.log_object && typeof data == "object") + if (typeof data.toString == "function") + return String(data) + else + return "[object null]" + + return util.inspect(data, { + depth: null, + colors: true, + showHidden: true, + showProxy: true, + getters: true, + breakLength: 100, + maxArrayLength: 100, + maxStringLength: 1000, + ...cfg.bot.log_object, + }) } - Buffer(data, opts = {}) { + async Buffer(data, opts = {}) { if (Buffer.isBuffer(data)) return data data = this.String(data) @@ -94,43 +301,48 @@ export default class Yunzai extends EventEmitter { return Buffer.from(data.replace(/^base64:\/\//, ""), "base64") } else if (data.match(/^https?:\/\//)) { if (opts.http) return data - return (async () => Buffer.from(await (await fetch(data)).arrayBuffer()))() - } else if (fs.existsSync(data.replace(/^file:\/\//, ""))) { + return Buffer.from(await (await fetch(data)).arrayBuffer()) + } else if (await this.fsStat(data.replace(/^file:\/\//, ""))) { if (opts.file) return data - return Buffer.from(fs.readFileSync(data.replace(/^file:\/\//, ""))) + return Buffer.from(await fs.readFile(data.replace(/^file:\/\//, ""))) } return data } - async fileType(data, name) { - const file = {} + async fileType(data, opts = {}) { + const file = { name: data.name } try { - if (Buffer.isBuffer(data)) { - file.url = name || "Buffer" - file.buffer = data + if (Buffer.isBuffer(data.file)) { + file.url = data.name || "Buffer" + file.buffer = data.file } else { - file.url = data.replace(/^base64:\/\/.*/, "base64://...") - file.buffer = await this.Buffer(data) + file.url = data.file.replace(/^base64:\/\/.*/, "base64://...") + file.buffer = await this.Buffer(data.file, opts) } if (Buffer.isBuffer(file.buffer)) { file.type = await fileTypeFromBuffer(file.buffer) file.md5 = md5(file.buffer) - file.name = name || `${Date.now()}.${file.md5.slice(0,8)}.${file.type.ext}` - } else { - file.name = name || `${Date.now()}-path.basename(file.buffer)` + if (!file.name) + file.name = `${Date.now()}.${file.md5.slice(0,8)}.${file.type.ext}` } } catch (err) { - logger.error(`文件类型检测错误:${logger.red(err)}`) + this.makeLog("error", ["文件类型检测错误", file, err]) } + if (!file.name) + file.name = `${Date.now()}-${path.basename(file.url)}` return file } async fileToUrl(file, opts = {}) { - const { name, time = 60000, times } = opts + const { + name, + time = cfg.bot.file_to_url_time*60000, + times = cfg.bot.file_to_url_times, + } = opts - file = await this.fileType(file, name) + file = await this.fileType({ file, name }, { http: true }) if (!Buffer.isBuffer(file.buffer)) return file.buffer - if (!file.name) file.name = randomUUID() + file.name = file.name ? encodeURIComponent(file.name) : randomUUID() if (typeof times == "number") file.times = times this.fs[file.name] = file @@ -144,25 +356,24 @@ export default class Yunzai extends EventEmitter { if (!file) file = this.fs[404] if (typeof file.times == "number") { - if (file.times > 0) file.times = file.times-1 + if (file.times > 0) file.times-- else file = this.fs.timeout } if (file.type?.mime) req.res.setHeader("Content-Type", file.type.mime) - logger.mark(`${logger.blue(`[${req.ip} => http://${req.headers.host}/File/${url}]`)} HTTP ${req.method} 请求:${JSON.stringify(req.headers)}`) - logger.mark(`${logger.blue(`[${req.ip} <= http://${req.headers.host}/File/${url}]`)} 发送文件:${file.name}(${file.url} ${(file.buffer.length/1024).toFixed(2)}KB)`) + this.makeLog("mark", `发送文件:${file.name}(${file.url} ${(file.buffer.length/1024).toFixed(2)}KB)`, `${req.sid} => ${req.rid}`) req.res.send(file.buffer) } async exec(cmd) { return new Promise(resolve => { - this.makeLog("mark", `[命令执行开始] ${logger.blue(cmd)}`) + this.makeLog("mark", `[执行命令] ${logger.blue(cmd)}`) exec(cmd, (error, stdout, stderr) => { - this.makeLog("mark", `[命令执行完成] ${logger.blue(cmd)}${stdout?`\n${this.String(stdout).trim()}`:""}${stderr?logger.red(`\n${this.String(stderr).trim()}`):""}`) - if (error) this.makeLog("mark", `[命令执行错误] ${logger.blue(cmd)}\n${logger.red(this.String(error).trim())}`) resolve({ error, stdout, stderr }) + this.makeLog("mark", `[执行命令完成] ${logger.blue(cmd)}${stdout?`\n${String(stdout).trim()}`:""}${stderr?logger.red(`\n${String(stderr).trim()}`):""}`) + if (error) this.makeLog("error", `[执行命令错误] ${logger.blue(cmd)}\n${logger.red(this.Loging(error).trim())}`) }) }) } @@ -170,18 +381,50 @@ export default class Yunzai extends EventEmitter { makeLog(level, msg, id) { const log = [] if (id) log.push(logger.blue(`[${id}]`)) - for (const i of Array.isArray(msg) ? msg : [msg]) { - if (i?.replace) - log.push(_.truncate(i.replace(/{"type":"Buffer","data":\[.*?\]}/g, "(Buffer)"), { length: cfg.bot.log_length })) - else - log.push(i) - } + for (const i of Array.isArray(msg) ? msg : [msg]) + log.push(_.truncate(this.Loging(i), { length: cfg.bot.log_length })) logger[level](...log) } + makeEvent(data) { + if (!this[data.self_id]) return + if (!data.bot) + Object.defineProperty(data, "bot", { + value: this[data.self_id], + }) + if (!data.friend && data.user_id) + Object.defineProperty(data, "friend", { + value: data.bot.pickFriend(data.user_id), + }) + if (!data.group && data.group_id) + Object.defineProperty(data, "group", { + value: data.bot.pickGroup(data.group_id), + }) + if (!data.member && data.group && data.user_id) + Object.defineProperty(data, "member", { + value: data.group.pickMember(data.user_id), + }) + + if (data.bot.adapter?.id) + data.adapter_id = data.bot.adapter.id + if (data.bot.adapter?.name) + data.adapter_name = data.bot.adapter.name + + for (const i of [data.friend, data.group, data.member]) { + if (typeof i != "object") continue + if (!i.sendFile) + i.sendFile = (file, name) => i.sendMsg(segment.file(file, name)) + if (!i.makeForwardMsg) + i.makeForwardMsg = this.makeForwardMsg + if (!i.sendForwardMsg) + i.sendForwardMsg = msg => this.sendForwardMsg(msg => i.sendMsg(msg), msg) + if (!i.getInfo) + i.getInfo = () => i + } + } + em(name = "", data = {}) { - if (data.self_id) - Object.defineProperty(data, "bot", { value: Bot[data.self_id] }) + this.makeEvent(data) while (true) { this.emit(name, data) const i = name.lastIndexOf(".") @@ -251,7 +494,7 @@ export default class Yunzai extends EventEmitter { user_id = Number(user_id) || String(user_id) const user = this.fl.get(user_id) if (user) return this[user.bot_id].pickFriend(user_id) - logger.error(`获取用户对象失败:找不到用户 ${logger.red(user_id)}`) + this.makeLog("error", ["获取用户对象错误:找不到用户", user_id]) } get pickUser() { return this.pickFriend } @@ -259,7 +502,7 @@ export default class Yunzai extends EventEmitter { group_id = Number(group_id) || String(group_id) const group = this.gl.get(group_id) if (group) return this[group.bot_id].pickGroup(group_id) - logger.error(`获取群对象失败:找不到群 ${logger.red(group_id)}`) + this.makeLog("error", ["获取群对象错误:找不到群", group_id]) } pickMember(group_id, user_id) { @@ -279,7 +522,7 @@ export default class Yunzai extends EventEmitter { this.once(`connect.${bot_id}`, data => resolve(data.bot.pickFriend(user_id).sendMsg(msg)))) } catch (err) { - logger.error(`${logger.blue(`[${bot_id}]`)} 发送好友消息失败:[$${user_id}] ${err}`) + this.makeLog("error", [`发送好友消息错误:[${user_id}]`, err], bot_id) } return false } @@ -296,7 +539,7 @@ export default class Yunzai extends EventEmitter { this.once(`connect.${bot_id}`, data => resolve(data.bot.pickGroup(group_id).sendMsg(msg)))) } catch (err) { - logger.error(`${logger.blue(`[${bot_id}]`)} 发送群消息失败:[$${group_id}] ${err}`) + this.makeLog("error", [`发送群消息错误:[${group_id}]`, err], bot_id) } return false } @@ -307,13 +550,13 @@ export default class Yunzai extends EventEmitter { fnc = data => data.self_id == self_id && data.user_id == user_id } - while (true) { + while (true) { try { const msg = await new Promise(resolve => { this.once("message", data => { if (data.message && fnc(data)) { let msg = "" for (const i of data.message) - if (i.type = "text") + if (i.type == "text" && i.text) msg += i.text.trim() resolve(msg) } else { @@ -322,7 +565,9 @@ export default class Yunzai extends EventEmitter { }) }) if (msg) return msg - } + } catch (err) { + this.makeLog("error", err) + }} } getMasterMsg() { @@ -338,10 +583,31 @@ export default class Yunzai extends EventEmitter { makeForwardMsg(msg) { return { type: "node", data: msg } } + makeForwardArray(msg = [], node = {}) { + const forward = [] + for (const message of Array.isArray(msg) ? msg : [msg]) + forward.push({ ...node, message }) + return this.makeForwardMsg(forward) + } + async sendForwardMsg(send, msg) { const messages = [] for (const { message } of msg) messages.push(await send(message)) return messages } + + getTimeDiff(time1 = this.stat.start_time, time2 = Date.now()/1000) { + const time = time2 - time1 + let ret = "" + const day = Math.floor(time / 3600 / 24) + if (day) ret += `${day}天` + const hour = Math.floor((time / 3600) % 24) + if (hour) ret += `${hour}时` + const min = Math.floor((time / 60) % 60) + if (min) ret += `${min}分` + const sec = Math.floor(time % 60) + if (sec) ret += `${sec}秒` + return ret || "0秒" + } } \ No newline at end of file diff --git a/Yunzai/lib/common/common.js b/Yunzai/lib/common/common.js index 90a2605fa512acb3da18114394d596c42ac0168e..dc47b98c7dd150fa2d15d647b1edaff2401bde05 100644 --- a/Yunzai/lib/common/common.js +++ b/Yunzai/lib/common/common.js @@ -41,13 +41,10 @@ async function downFile(fileUrl, savePath,param = {}) { } function mkdirs(dirname) { - if (fs.existsSync(dirname)) { + if (fs.existsSync(dirname)) return true + if (mkdirs(path.dirname(dirname))) { + fs.mkdirSync(dirname) return true - } else { - if (mkdirs(path.dirname(dirname))) { - fs.mkdirSync(dirname) - return true - } } } diff --git a/Yunzai/lib/config/config.js b/Yunzai/lib/config/config.js index fb315c44fe2b78989181e9d36183628cdce49f7a..fd00abf3a753596bb102980c111db848aa78bb3e 100644 --- a/Yunzai/lib/config/config.js +++ b/Yunzai/lib/config/config.js @@ -4,7 +4,7 @@ import chokidar from "chokidar" /** 配置文件 */ class Cfg { - constructor () { + constructor() { this.config = {} /** 监听文件 */ @@ -14,7 +14,7 @@ class Cfg { } /** 初始化配置 */ - initCfg () { + initCfg() { let path = "config/config/" let pathDef = "config/default_config/" const files = fs.readdirSync(pathDef).filter(file => file.endsWith(".yaml")) @@ -22,11 +22,12 @@ class Cfg { if (!fs.existsSync(`${path}${file}`)) fs.copyFileSync(`${pathDef}${file}`, `${path}${file}`) for (const i of ["data", "temp"]) - if (!fs.existsSync(i)) fs.mkdirSync(i) + if (!fs.existsSync(i)) + fs.mkdirSync(i) } /** Bot配置 */ - get bot () { + get bot() { let bot = this.getConfig("bot") let defbot = this.getdefSet("bot") bot = { ...defbot, ...bot } @@ -34,11 +35,11 @@ class Cfg { return bot } - get other () { + get other() { return this.getConfig("other") } - get redis () { + get redis() { return this.getConfig("redis") } @@ -47,7 +48,7 @@ class Cfg { } /** 主人账号 */ - get masterQQ () { + get masterQQ() { let masterQQ = this.getConfig("other").masterQQ || [] if (!Array.isArray(masterQQ)) @@ -60,7 +61,7 @@ class Cfg { } /** Bot账号:[主人帐号] */ - get master () { + get master() { let master = this.getConfig("other").master || [] if (!Array.isArray(master)) @@ -80,15 +81,15 @@ class Cfg { } /** 机器人账号 */ - get uin () { + get uin() { return Object.keys(this.master) } - get qq () { + get qq() { return this.uin } /** package.json */ - get package () { + get package() { if (this._package) return this._package this._package = JSON.parse(fs.readFileSync("package.json", "utf8")) @@ -96,7 +97,7 @@ class Cfg { } /** 群配置 */ - getGroup (bot_id = "", group_id = "") { + getGroup(bot_id = "", group_id = "") { const config = this.getConfig("group") const defCfg = this.getdefSet("group") return { @@ -109,7 +110,7 @@ class Cfg { } /** other配置 */ - getOther () { + getOther() { let def = this.getdefSet("other") let config = this.getConfig("other") return { ...def, ...config } @@ -119,12 +120,12 @@ class Cfg { * @param app 功能 * @param name 配置文件名称 */ - getdefSet (name) { + getdefSet(name) { return this.getYaml("default_config", name) } /** 用户配置 */ - getConfig (name) { + getConfig(name) { return this.getYaml("config", name) } @@ -133,7 +134,7 @@ class Cfg { * @param type 默认跑配置-defSet,用户配置-config * @param name 名称 */ - getYaml (type, name) { + getYaml(type, name) { let file = `config/${type}/${name}.yaml` let key = `${type}.${name}` if (this.config[key]) return this.config[key] @@ -148,7 +149,7 @@ class Cfg { } /** 监听配置文件 */ - watch (file, name, type = "default_config") { + watch(file, name, type = "default_config") { let key = `${type}.${name}` if (this.watcher[key]) return @@ -166,7 +167,7 @@ class Cfg { this.watcher[key] = watcher } - async change_bot () { + async change_bot() { /** 修改日志等级 */ let log = await import("./log.js") log.default() diff --git a/Yunzai/lib/config/init.js b/Yunzai/lib/config/init.js index c0842fba171224efd4fec042ecbbe319000a8b71..a1005ed23b4bf994962764d228e1733313d88fb4 100644 --- a/Yunzai/lib/config/init.js +++ b/Yunzai/lib/config/init.js @@ -1,6 +1,4 @@ import setLog from "./log.js" -import redisInit from "./redis.js" -import { checkRun } from "./check.js" import cfg from "./config.js" /** 设置标题 */ @@ -9,41 +7,30 @@ process.title = `TRSS Yunzai v${cfg.package.version} © 2023 - 2024 TimeRainStar /** 设置时区 */ process.env.TZ = "Asia/Shanghai" -/** 捕获未处理的错误 */ -process.on("uncaughtException", error => { - if (typeof logger == "undefined") console.log(error) - else logger.error(error) -}) - -/** 捕获未处理的Promise错误 */ -process.on("unhandledRejection", (error, promise) => { - if (typeof logger == "undefined") console.log(error) - else logger.error(error) -}) - -/** 退出事件 */ -process.on("exit", async code => { - if (typeof redis != "undefined" && typeof test == "undefined") - await redis.save() - - if (typeof logger == "undefined") - console.log("TRSS-Yunzai 已停止运行") - else - logger.mark(logger.magenta("TRSS-Yunzai 已停止运行")) -}) +process.on("SIGHUP", () => process.exit()) -await checkInit() +/** 日志设置 */ +setLog() -/** 初始化事件 */ -async function checkInit() { - /** 日志设置 */ - setLog() +/** 捕获未处理的错误 */ +for (const i of ["uncaughtException", "unhandledRejection"]) + process.on(i, e => { + try { + Bot.makeLog("error", e, i) + } catch (err) { + console.error(i, e, err) + process.exit() + } + }) - logger.mark("----^_^----") - logger.mark(logger.yellow(`TRSS-Yunzai v${cfg.package.version} 启动中...`)) - logger.mark(logger.cyan("https://github.com/TimeRainStarSky/Yunzai")) +/** 退出事件 */ +process.on("exit", code => { + if (typeof redis != "undefined") + redis.save() - await redisInit() + logger.mark(logger.magenta(`TRSS-Yunzai 已停止运行,本次运行时长:${Bot.getTimeDiff()} (${code})`)) +}) - checkRun() -} \ No newline at end of file +logger.mark("----^_^----") +logger.mark(logger.yellow(`TRSS-Yunzai v${cfg.package.version} 启动中...`)) +logger.mark(logger.cyan("https://github.com/TimeRainStarSky/Yunzai")) \ No newline at end of file diff --git a/Yunzai/lib/config/log.js b/Yunzai/lib/config/log.js index e957e5261db637918df4afc640dcd90a885b556a..698d8c14dc0cfe6f3a85871f954c2fd9afe4d1ce 100644 --- a/Yunzai/lib/config/log.js +++ b/Yunzai/lib/config/log.js @@ -1,16 +1,15 @@ -import log4js from 'log4js' -import chalk from 'chalk' -import cfg from './config.js' -import fs from 'node:fs' +import log4js from "log4js" +import chalk from "chalk" +import cfg from "./config.js" +import fs from "node:fs" /** * 设置日志样式 */ -export default function setLog () { - let file = './logs' - if (!fs.existsSync(file)) { +export default function setLog() { + let file = "./logs" + if (!fs.existsSync(file)) fs.mkdirSync(file) - } /** 调整error日志等级 */ // log4js.levels.levels[5].level = Number.MAX_VALUE @@ -19,80 +18,59 @@ export default function setLog () { log4js.configure({ appenders: { console: { - type: 'console', + type: "console", layout: { - type: 'pattern', - pattern: '%[[TRSSYz][%d{hh:mm:ss.SSS}][%4.4p]%] %m' + type: "pattern", + pattern: "%[[TRSSYz][%d{hh:mm:ss.SSS}][%4.4p]%] %m" } }, command: { - type: 'dateFile', // 可以是console,dateFile,file,Logstash等 - filename: 'logs/command', // 将会按照filename和pattern拼接文件名 - pattern: 'yyyy-MM-dd.log', + type: "dateFile", // 可以是console,dateFile,file,Logstash等 + filename: "logs/command", // 将会按照filename和pattern拼接文件名 + pattern: "yyyy-MM-dd.log", numBackups: 15, alwaysIncludePattern: true, layout: { - type: 'pattern', - pattern: '[%d{hh:mm:ss.SSS}][%4.4p] %m' + type: "pattern", + pattern: "[%d{hh:mm:ss.SSS}][%4.4p] %m" } }, error: { - type: 'file', - filename: 'logs/error.log', + type: "file", + filename: "logs/error.log", alwaysIncludePattern: true, layout: { - type: 'pattern', - pattern: '[%d{hh:mm:ss.SSS}][%4.4p] %m' + type: "pattern", + pattern: "[%d{hh:mm:ss.SSS}][%4.4p] %m" } } }, categories: { - default: { appenders: ['console'], level: cfg.bot.log_level }, - command: { appenders: ['console', 'command'], level: 'warn' }, - error: { appenders: ['console', 'command', 'error'], level: 'error' } + default: { appenders: ["console"], level: cfg.bot.log_level }, + command: { appenders: ["console", "command"], level: "warn" }, + error: { appenders: ["console", "command", "error"], level: "error" } } }) - const defaultLogger = log4js.getLogger('message') - const commandLogger = log4js.getLogger('command') - const errorLogger = log4js.getLogger('error') + const defaultLogger = log4js.getLogger("message") + const commandLogger = log4js.getLogger("command") + const errorLogger = log4js.getLogger("error") - /* eslint-disable no-useless-call */ /** 全局变量 logger */ global.logger = { - trace () { - defaultLogger.trace.call(defaultLogger, ...arguments) - }, - debug () { - defaultLogger.debug.call(defaultLogger, ...arguments) - }, - info () { - defaultLogger.info.call(defaultLogger, ...arguments) - }, - // warn及以上的日志采用error策略 - warn () { - commandLogger.warn.call(defaultLogger, ...arguments) - }, - error () { - errorLogger.error.call(errorLogger, ...arguments) - }, - fatal () { - errorLogger.fatal.call(errorLogger, ...arguments) - }, - mark () { - errorLogger.mark.call(commandLogger, ...arguments) - } + trace: (...args) => defaultLogger.trace(...args), + debug: (...args) => defaultLogger.debug(...args), + info: (...args) => defaultLogger.info(...args), + warn: (...args) => commandLogger.warn(...args), + error: (...args) => errorLogger.error(...args), + fatal: (...args) => errorLogger.fatal(...args), + mark: (...args) => commandLogger.mark(...args), + chalk: chalk, + red: chalk.red, + green: chalk.green, + yellow: chalk.yellow, + blue: chalk.blue, + magenta: chalk.magenta, + cyan: chalk.cyan, } - - logColor() -} - -function logColor () { - logger.chalk = chalk - logger.red = chalk.red - logger.green = chalk.green - logger.yellow = chalk.yellow - logger.blue = chalk.blue - logger.magenta = chalk.magenta - logger.cyan = chalk.cyan -} +} \ No newline at end of file diff --git a/Yunzai/lib/config/redis.js b/Yunzai/lib/config/redis.js index aad2882805b7caf6b611e9cd01ec0ec33069e322..030607795cc2ee484e5c2fe237a9d99eb9c826e7 100644 --- a/Yunzai/lib/config/redis.js +++ b/Yunzai/lib/config/redis.js @@ -1,8 +1,8 @@ import cfg from "./config.js" -import common from "../common/common.js" import { createClient } from "redis" import { exec } from "node:child_process" +let lock = false /** * 初始化全局redis客户端 */ @@ -13,65 +13,54 @@ export default async function redisInit() { if (rc.username || rc.password) redisPw += "@" const redisUrl = `redis://${redisUn}${redisPw}${rc.host}:${rc.port}/${rc.db}` - let client = createClient({ url: redisUrl }) + Bot.makeLog("info", `正在连接 ${logger.blue(redisUrl)}`, "Redis") + return connectRedis(redisUrl) +} + +async function connectRedis(redisUrl, cmd) { + if (lock && !cmd) return + lock = true try { - logger.info(`正在连接 ${logger.blue(redisUrl)}`) - await client.connect() + global.redis = createClient({ url: redisUrl }) + await redis.connect() } catch (err) { - logger.error(`Redis 错误:${logger.red(err)}`) - - const cmd = "redis-server --save 900 1 --save 300 10 --daemonize yes" + await aarch64() - logger.info("正在启动 Redis...") - await execSync(cmd) - await common.sleep(1000) - - try { - client = createClient({ url: redisUrl }) - await client.connect() - } catch (err) { - logger.error(`Redis 错误:${logger.red(err)}`) - logger.error(`请先启动 Redis:${logger.blue(cmd)}`) - process.exit() - } + Bot.makeLog("error", ["连接错误", err], "Redis") + if (!cmd) return startRedis(redisUrl) + Bot.makeLog("error", ["请先启动", logger.blue(cmd)], "Redis") + process.exit(1) } - client.on("error", async err => { - logger.error(`Redis 错误:${logger.red(err)}`) - const cmd = "redis-server --save 900 1 --save 300 10 --daemonize yes" + await aarch64() - logger.error(`请先启动 Redis:${cmd}`) - process.exit() + redis.on("error", err => { + Bot.makeLog("error", err, "Redis") + return connectRedis(redisUrl) }) - /** 全局变量 redis */ - client.url = redisUrl - global.redis = client - logger.info("Redis 连接成功") - return client + lock = false + return redis +} + +async function startRedis(redisUrl) { + if (cfg.redis.host != "127.0.0.1") { + Bot.makeLog("error", `连接失败,请确认连接地址正确`, "Redis") + process.exit(1) + } + const cmd = `redis-server --port ${cfg.redis.port} --save 900 1 --save 300 10 --daemonize yes${await aarch64()}` + await Bot.exec(cmd) + await Bot.sleep(1000) + return connectRedis(redisUrl, cmd) } async function aarch64() { - if (process.platform == "win32") + if (process.platform == "win32" || process.arch != "arm64") return "" - /** 判断arch */ - const arch = await execSync("uname -m") - if (arch.stdout && arch.stdout.includes("aarch64")) { - /** 判断redis版本 */ - let v = await execSync("redis-server -v") - if (v.stdout) { - v = v.stdout.match(/v=(\d)./) - /** 忽略arm警告 */ - if (v && v[1] >= 6) - return " --ignore-warnings ARM64-COW-BUG" - } + /** 判断redis版本 */ + let v = await Bot.exec("redis-server -v") + if (v.stdout?.match) { + v = v.stdout.match(/v=(\d)./) + /** 忽略arm警告 */ + if (v && v[1] >= 6) + return " --ignore-warnings ARM64-COW-BUG" } return "" -} - -function execSync (cmd) { - return new Promise((resolve, reject) => { - exec(cmd, (error, stdout, stderr) => { - resolve({ error, stdout, stderr }) - }) - }) } \ No newline at end of file diff --git a/Yunzai/lib/events/message.js b/Yunzai/lib/events/message.js index 8084c076bdc9574890a4fab98da03f20879df5f3..23686dda1ebb259b96f2bb22aeab52d8d0561fbc 100644 --- a/Yunzai/lib/events/message.js +++ b/Yunzai/lib/events/message.js @@ -1,11 +1,11 @@ -import EventListener from '../listener/listener.js' +import EventListener from "../listener/listener.js" /** * 监听群聊消息 */ export default class messageEvent extends EventListener { constructor () { - super({ event: 'message' }) + super({ event: "message" }) } async execute (e) { diff --git a/Yunzai/lib/events/notice.js b/Yunzai/lib/events/notice.js index 8e42c604303afb95bfb207fafabdf98f59fb6460..2be29fc171e7c277b00ab10298dbf63838bb15c8 100644 --- a/Yunzai/lib/events/notice.js +++ b/Yunzai/lib/events/notice.js @@ -1,11 +1,11 @@ -import EventListener from '../listener/listener.js' +import EventListener from "../listener/listener.js" /** * 监听群聊消息 */ export default class noticeEvent extends EventListener { constructor () { - super({ event: 'notice' }) + super({ event: "notice" }) } async execute (e) { diff --git a/Yunzai/lib/events/online.js b/Yunzai/lib/events/online.js index 7635342dea07cb76a00943ad6a38cdc29e4bf632..a6dfd6b088c21046e08b486ed38dd4a81e9383a9 100644 --- a/Yunzai/lib/events/online.js +++ b/Yunzai/lib/events/online.js @@ -1,5 +1,4 @@ -import EventListener from '../listener/listener.js' -import cfg from '../config/config.js' +import EventListener from "../listener/listener.js" /** * 监听上线事件 @@ -7,12 +6,12 @@ import cfg from '../config/config.js' export default class onlineEvent extends EventListener { constructor() { super({ - event: 'online', + event: "online", once: true }) } async execute() { - logger.mark('----^_^----') + logger.mark("----^_^----") } } \ No newline at end of file diff --git a/Yunzai/lib/events/request.js b/Yunzai/lib/events/request.js index 44338f7ab4a7e21d2ff0a0f61d2bb575be3c7074..c4122fb82322bca0dd5393e3eac16260913410f5 100644 --- a/Yunzai/lib/events/request.js +++ b/Yunzai/lib/events/request.js @@ -1,11 +1,11 @@ -import EventListener from '../listener/listener.js' +import EventListener from "../listener/listener.js" /** * 监听群聊消息 */ export default class requestEvent extends EventListener { constructor () { - super({ event: 'request' }) + super({ event: "request" }) } async execute (e) { diff --git a/Yunzai/lib/listener/listener.js b/Yunzai/lib/listener/listener.js index 644f7a1bb5ee78279807bce45a6733f333274b74..c8ed7165f45de52903ed4fc0799ae5865d31afae 100644 --- a/Yunzai/lib/listener/listener.js +++ b/Yunzai/lib/listener/listener.js @@ -1,4 +1,4 @@ -import PluginsLoader from '../plugins/loader.js' +import PluginsLoader from "../plugins/loader.js" export default class EventListener { /** @@ -8,7 +8,7 @@ export default class EventListener { * @param data.once 是否只监听一次 */ constructor (data) { - this.prefix = data.prefix || '' + this.prefix = data.prefix || "" this.event = data.event this.once = data.once || false this.plugins = PluginsLoader diff --git a/Yunzai/lib/listener/loader.js b/Yunzai/lib/listener/loader.js index cbed4fd1ed8fb5a0f2eddfc25c8109bb5d1b69ea..337d69c9c96bc2e47711be3193269687775e5645 100644 --- a/Yunzai/lib/listener/loader.js +++ b/Yunzai/lib/listener/loader.js @@ -1,5 +1,5 @@ -import fs from 'node:fs' -import lodash from 'lodash' +import fs from "node:fs/promises" +import lodash from "lodash" /** * 加载监听事件 @@ -12,21 +12,21 @@ class ListenerLoader { logger.info("-----------") logger.info("加载监听事件中...") let eventCount = 0 - for (const file of fs.readdirSync('./lib/events').filter(file => file.endsWith('.js'))) { + for (const file of (await fs.readdir("./lib/events")).filter(file => file.endsWith(".js"))) { logger.debug(`加载监听事件:${file}`) try { let listener = await import(`../events/${file}`) if (!listener.default) continue listener = new listener.default() - const on = listener.once ? 'once' : 'on' + const on = listener.once ? "once" : "on" if (lodash.isArray(listener.event)) { listener.event.forEach((type) => { - const e = listener[type] ? type : 'execute' + const e = listener[type] ? type : "execute" Bot[on](listener.prefix + type, event => listener[e](event)) }) } else { - const e = listener[listener.event] ? listener.event : 'execute' + const e = listener[listener.event] ? listener.event : "execute" Bot[on](listener.prefix + listener.event, event => listener[e](event)) } eventCount++ diff --git a/Yunzai/lib/modules/md5/index.js b/Yunzai/lib/modules/md5/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3bbe36b2c9db5ee60b9c0912de0683a754b9373f --- /dev/null +++ b/Yunzai/lib/modules/md5/index.js @@ -0,0 +1,4 @@ +import { createHash } from "node:crypto" +export default function md5(data) { + return createHash("md5").update(data).digest("hex") +} \ No newline at end of file diff --git a/Yunzai/lib/modules/md5/package.json b/Yunzai/lib/modules/md5/package.json new file mode 100644 index 0000000000000000000000000000000000000000..c86f3f82a9104dfdacb597042628028a3d04bc8f --- /dev/null +++ b/Yunzai/lib/modules/md5/package.json @@ -0,0 +1,5 @@ +{ + "name": "md5", + "type": "module", + "main": "index.js" +} \ No newline at end of file diff --git a/Yunzai/lib/modules/node-fetch/index.js b/Yunzai/lib/modules/node-fetch/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ebf6f0012a0929cc12380af47f9d14e955ca4808 --- /dev/null +++ b/Yunzai/lib/modules/node-fetch/index.js @@ -0,0 +1,2 @@ +export const { Blob, File, FormData, Headers, Request, Response } = global +export default global.fetch \ No newline at end of file diff --git a/Yunzai/lib/modules/node-fetch/package.json b/Yunzai/lib/modules/node-fetch/package.json new file mode 100644 index 0000000000000000000000000000000000000000..2eb220d10700181d98ecce7ea58bca8a052cae8e --- /dev/null +++ b/Yunzai/lib/modules/node-fetch/package.json @@ -0,0 +1,5 @@ +{ + "name": "node-fetch", + "type": "module", + "main": "index.js" +} \ No newline at end of file diff --git a/Yunzai/lib/modules/oicq/index.js b/Yunzai/lib/modules/oicq/index.js index 3f429567f5bb16f373d6c5ce2d7b372dc9bd163e..64dd3e370ec1e8c9a6fcf1f67e2c29d419bf2392 100644 --- a/Yunzai/lib/modules/oicq/index.js +++ b/Yunzai/lib/modules/oicq/index.js @@ -1,67 +1,33 @@ -import fs from "node:fs" -import path from "node:path" - -function toSegment(type, data) { - for (const i in data) - if (typeof data[i] == "string" && (i == "file" || data[i].match(/^file:\/\//)) && fs.existsSync(data[i].replace(/^file:\/\//, ""))) { - if (i == "file" && !data.name) - data.name = `${Date.now()}-${path.basename(data[i])}` - data[i] = fs.readFileSync(data[i].replace(/^file:\/\//, "")) - } - return { type, ...data } -} - const segment = new class segment { custom(type, data) { - return toSegment(type, data) + return { type, ...data } } raw(data) { - return toSegment("raw", { data }) + return { type: "raw", data } } button(...data) { - return toSegment("button", { data }) + return { type: "button", data } } markdown(data) { - return toSegment("markdown", { data }) + return { type: "markdown", data } } image(file, name) { - return toSegment("image", { file, name }) + return { type: "image", file, name } } at(qq, name) { - return toSegment("at", { qq, name }) + return { type: "at", qq, name } } record(file, name) { - return toSegment("record", { file, name }) + return { type: "record", file, name } } video(file, name) { - return toSegment("video", { file, name }) + return { type: "video", file, name } } file(file, name) { - return toSegment("file", { file, name }) + return { type: "file", file, name } } reply(id, text, qq, time, seq) { - return toSegment("reply", { id, text, qq, time, seq }) - } - face(id) { - return toSegment("face", { id }) - } - share(url, title, content, image) { - return toSegment("share", { url, title, content, image }) - } - music(type, id, url, audio, title) { - return toSegment("music", { type, id, url, audio, title }) - } - poke(qq) { - return toSegment("poke", { qq }) - } - gift(qq, id) { - return toSegment("gift", { qq, id }) - } - cardimage(file, name, minwidth, minheight, maxwidth, maxheight, source, icon) { - return toSegment("cardimage", { file, name, minwidth, minheight, maxwidth, maxheight, source, icon }) - } - tts(text) { - return toSegment("tts", { text }) + return { type: "reply", id, text, qq, time, seq } } } diff --git a/Yunzai/lib/plugins/config.js b/Yunzai/lib/plugins/config.js new file mode 100644 index 0000000000000000000000000000000000000000..621ec117a96e3885ebc327f3da866ac0ebd5ba54 --- /dev/null +++ b/Yunzai/lib/plugins/config.js @@ -0,0 +1,20 @@ +import fs from "node:fs/promises" +import YAML from "yaml" +import _ from "lodash" +export default async function(name, config, keep) { + const configFile = `config/${name}.yaml` + const configSave = () => fs.writeFile(configFile, YAML.stringify(config), "utf-8") + + let configData + try { + configData = YAML.parse(await fs.readFile(configFile, "utf-8")) + _.merge(config, configData) + } catch (err) { + logger.debug("配置文件", configFile, "读取失败", err) + } + _.merge(config, keep) + + if (YAML.stringify(config) != YAML.stringify(configData)) + await configSave() + return { config, configSave } +} \ No newline at end of file diff --git a/Yunzai/lib/plugins/loader.js b/Yunzai/lib/plugins/loader.js index d9519f68b8b31e9c8f3b1257451a718a5d37e0e1..335bc5bef8c67a6f4e13117a35e30a565ed7dea8 100644 --- a/Yunzai/lib/plugins/loader.js +++ b/Yunzai/lib/plugins/loader.js @@ -1,5 +1,5 @@ import util from "node:util" -import fs from "node:fs" +import fs from "node:fs/promises" import lodash from "lodash" import cfg from "../config/config.js" import plugin from "./plugin.js" @@ -8,9 +8,8 @@ import { segment } from "oicq" import chokidar from "chokidar" import moment from "moment" import path from "node:path" -import common from "../common/common.js" import Runtime from "./runtime.js" -import Handler from './handler.js' +import Handler from "./handler.js" /** 全局变量 plugin */ global.plugin = plugin @@ -24,7 +23,7 @@ class PluginsLoader { this.priority = [] this.handler = {} this.task = [] - this.dir = "./plugins" + this.dir = "plugins" /** 命令冷却cd */ this.groupCD = {} @@ -32,6 +31,11 @@ class PluginsLoader { /** 插件监听 */ this.watcher = {} + this.eventMap = { + message: ["post_type", "message_type", "sub_type"], + notice: ["post_type", "notice_type", "sub_type"], + request: ["post_type", "request_type", "sub_type"], + } this.msgThrottle = {} @@ -39,133 +43,127 @@ class PluginsLoader { this.srReg = /^#?(\*|星铁|星轨|穹轨|星穹|崩铁|星穹铁道|崩坏星穹铁道|铁道)+/ } + async getPlugins() { + const files = await fs.readdir(this.dir, { withFileTypes: true }) + const ret = [] + for (const val of files) { + if (val.isFile()) continue + const tmp = { + name: val.name, + path: `../../${this.dir}/${val.name}`, + } + + if (await Bot.fsStat(`${this.dir}/${val.name}/index.js`)) { + tmp.path = `${tmp.path}/index.js` + ret.push(tmp) + continue + } + + const apps = await fs.readdir(`${this.dir}/${val.name}`, { withFileTypes: true }) + for (const app of apps) { + if (!app.isFile()) continue + if (!app.name.endsWith(".js")) continue + ret.push({ + name: `${tmp.name}/${app.name}`, + path: `${tmp.path}/${app.name}`, + }) + /** 监听热更新 */ + this.watch(val.name, app.name) + } + } + return ret + } + /** * 监听事件加载 * @param isRefresh 是否刷新 */ async load(isRefresh = false) { - this.delCount() - if (!lodash.isEmpty(this.priority) && !isRefresh) return - - const files = this.getPlugins() + if (isRefresh) this.priority = [] + if (this.priority.length) return logger.info("-----------") logger.info("加载插件中...") - let pluCount = 0 + const files = await this.getPlugins() + this.pluginCount = 0 + const packageErr = [] - let packageErr = [] - for (let File of files) { - try { - let tmp = await import(File.path) - let apps = tmp - if (tmp.apps) { - apps = { ...tmp.apps } - } - lodash.forEach(apps, (p, i) => { - if (!p.prototype) return - pluCount++ - /* eslint-disable new-cap */ - let plugin = new p() - logger.debug(`载入插件 [${File.name}][${plugin.name}]`) - /** 执行初始化 */ - this.runInit(plugin) - /** 初始化定时任务 */ - this.collectTask(plugin.task) - this.priority.push({ - class: p, - key: File.name, - name: plugin.name, - priority: plugin.priority - }) - if (plugin.handler) { - lodash.forEach(plugin.handler, ({ fn, key, priority }) => { - Handler.add({ - ns: plugin.namespace || File.name, - key: key, - self: plugin, - property: priority || plugin.priority || 500, - fn: plugin[fn] - }) - }) - } - }) - } catch (error) { - if (error.stack.includes("Cannot find package")) { - packageErr.push({ error, File }) - } else { - logger.error(`载入插件错误:${logger.red(File.name)}`) - logger.error(decodeURI(error.stack)) - } - } - } + await Promise.allSettled(files.map(file => + this.importPlugin(file, packageErr) + )) this.packageTips(packageErr) - this.creatTask() + this.createTask() logger.info(`加载定时任务[${this.task.length}个]`) - logger.info(`加载插件[${pluCount}个]`) + logger.info(`加载插件[${this.pluginCount}个]`) /** 优先级排序 */ this.priority = lodash.orderBy(this.priority, ["priority"], ["asc"]) } - async runInit(plugin) { - plugin.init && plugin.init() + async importPlugin(file, packageErr) { + try { + let app = await import(file.path) + if (app.apps) app = { ...app.apps } + const pluginArray = [] + lodash.forEach(app, p => + pluginArray.push(this.loadPlugin(file, p)) + ) + for (const i of await Promise.allSettled(pluginArray)) + if (i?.status && i.status != "fulfilled") { + logger.error(`加载插件错误:${logger.red(file.name)}`) + logger.error(decodeURI(i.reason)) + } + } catch (error) { + if (packageErr && error.stack.includes("Cannot find package")) { + packageErr.push({ error, file }) + } else { + logger.error(`加载插件错误:${logger.red(file.name)}`) + logger.error(decodeURI(error.stack)) + } + } } - packageTips(packageErr) { - if (!packageErr || packageErr.length <= 0) return - logger.mark("--------插件载入错误--------") - packageErr.forEach(v => { - let pack = v.error.stack.match(/'(.+?)'/g)[0].replace(/'/g, "") - logger.mark(`${v.File.name} 缺少依赖:${logger.red(pack)}`) - logger.mark(`新增插件后请执行安装命令:${logger.red("pnpm i")} 安装依赖`) - logger.mark("如安装后仍未解决可联系插件作者解决") + async loadPlugin(file, p) { + if (!p?.prototype) return + this.pluginCount++ + const plugin = new p + logger.debug(`加载插件 [${file.name}][${plugin.name}]`) + /** 执行初始化,返回 return 则跳过加载 */ + if (plugin.init && await plugin.init() == "return") return + /** 初始化定时任务 */ + this.collectTask(plugin.task) + this.priority.push({ + class: p, + key: file.name, + name: plugin.name, + priority: plugin.priority }) - // logger.error("或者使用其他包管理工具安装依赖") - logger.mark("---------------------") - } - - getPlugins() { - let ignore = ["index.js"] - let files = fs.readdirSync(this.dir, { withFileTypes: true }) - let ret = [] - for (let val of files) { - let filepath = "../../plugins/" + val.name - let tmp = { - name: val.name - } - if (val.isFile()) { - if (!val.name.endsWith(".js")) continue - if (ignore.includes(val.name)) continue - tmp.path = filepath - ret.push(tmp) - continue - } - - if (fs.existsSync(`${this.dir}/${val.name}/index.js`)) { - tmp.path = filepath + "/index.js" - ret.push(tmp) - continue - } - - let apps = fs.readdirSync(`${this.dir}/${val.name}`, { withFileTypes: true }) - for (let app of apps) { - if (!app.name.endsWith(".js")) continue - if (ignore.includes(app.name)) continue - - ret.push({ - name: `${val.name}/${app.name}`, - path: `../../plugins/${val.name}/${app.name}` + if (plugin.handler) { + lodash.forEach(plugin.handler, ({ fn, key, priority }) => { + Handler.add({ + ns: plugin.namespace || file.name, + key, + self: plugin, + property: priority || plugin.priority || 500, + fn: plugin[fn] }) - - /** 监听热更新 */ - this.watch(val.name, app.name) - } + }) } + } - return ret + packageTips(packageErr) { + if (!packageErr.length) return + logger.mark("--------- 插件加载错误 ---------") + for (const i of packageErr) { + const pack = i.error.stack.match(/'(.+?)'/g)[0].replace(/'/g, "") + logger.mark(`${logger.blue(i.file.name)} 缺少依赖 ${logger.red(pack)}`) + } + logger.mark(`安装插件后请 ${logger.red("pnpm i")} 安装依赖`) + logger.mark(`仍报错${logger.red("进入插件目录")} pnpm add 依赖`) + logger.mark("--------------------------------") } /** @@ -175,52 +173,38 @@ class PluginsLoader { * @param e 事件 */ async deal(e) { + this.count(e, "receive", e.message) /** 检查黑白名单 */ if (!this.checkBlack(e)) return /** 冷却 */ if (!this.checkLimit(e)) return /** 处理事件 */ this.dealEvent(e) - /** 处理消息 */ - this.dealMsg(e) /** 处理回复 */ this.reply(e) - /** 过滤事件 */ - let priority = [] /** 注册runtime */ await Runtime.init(e) - this.priority.forEach(v => { - let p = new v.class(e) + const priority = [] + for (const i of this.priority) { + const p = new i.class(e) p.e = e - /** 判断是否启用功能 */ - if (!this.checkDisable(e, p)) return - /** 过滤事件 */ - if (!this.filtEvent(e, p)) return - priority.push(p) - }) + /** 判断是否启用功能,过滤事件 */ + if (this.checkDisable(p) && this.filtEvent(e, p)) + priority.push(p) + } - for (let plugin of priority) { + for (const plugin of priority) { /** 上下文hook */ - if (plugin.getContext) { - let context = plugin.getContext() - if (!lodash.isEmpty(context)) { - for (let fnc in context) { - plugin[fnc](context[fnc]) - } - return - } + if (!plugin.getContext) continue + const context = { + ...plugin.getContext(), + ...plugin.getContext(false, true), } - - /** 群上下文hook */ - if (plugin.getContextGroup) { - let context = plugin.getContextGroup() - if (!lodash.isEmpty(context)) { - for (let fnc in context) { - plugin[fnc](context[fnc]) - } - return - } + if (!lodash.isEmpty(context)) { + for (const fnc in context) + plugin[fnc](context[fnc]) + return } } @@ -242,59 +226,43 @@ class PluginsLoader { e.msg = e.msg.replace(this.srReg, "#星铁") } - /** accept */ - for (let plugin of priority) { - /** accept hook */ + /** 优先执行 accept */ + for (const plugin of priority) if (plugin.accept) { - let res = plugin.accept(e) - - if (util.types.isPromise(res)) res = await res - - if (res === "return") return - + const res = await plugin.accept(e) + if (res == "return") return if (res) break } - } - /* eslint-disable no-labels */ - a: for (let plugin of priority) { + a: for (const plugin of priority) { /** 正则匹配 */ - if (plugin.rule) { - for (let v of plugin.rule) { - /** 判断事件 */ - if (v.event && !this.filtEvent(e, v)) continue - - if (new RegExp(v.reg).test(e.msg)) { - e.logFnc = `[${plugin.name}][${v.fnc}]` - - if (v.log !== false) { - logger.info(`${e.logFnc}${e.logText} ${lodash.truncate(e.msg, { length: 80 })}`) - } - - /** 判断权限 */ - if (!this.filtPermission(e, v)) break a - - try { - let res = plugin[v.fnc] && plugin[v.fnc](e) - - let start = Date.now() - - if (util.types.isPromise(res)) res = await res - - if (res !== false) { - /** 设置冷却cd */ - this.setLimit(e) - if (v.log !== false) { - logger.mark(`${e.logFnc} ${lodash.truncate(e.msg, { length: 80 })} 处理完成 ${Date.now() - start}ms`) - } - break a - } - } catch (error) { - logger.error(`${e.logFnc}`) - logger.error(error.stack) - break a - } + if (plugin.rule) for (const v of plugin.rule) { + /** 判断事件 */ + if (v.event && !this.filtEvent(e, v)) continue + + if (!new RegExp(v.reg).test(e.msg)) continue + e.logFnc = `[${plugin.name}][${v.fnc}]` + + if (v.log !== false) + logger.info(`${e.logFnc}${e.logText} ${lodash.truncate(e.msg, { length: 100 })}`) + + /** 判断权限 */ + if (!this.filtPermission(e, v)) break a + + try { + const start = Date.now() + const res = plugin[v.fnc] && (await plugin[v.fnc](e)) + if (res !== false) { + /** 设置冷却cd */ + this.setLimit(e) + if (v.log !== false) + logger.mark(`${e.logFnc} ${lodash.truncate(e.msg, { length: 100 })} 处理完成 ${Date.now() - start}ms`) + break a } + } catch (error) { + logger.error(`${e.logFnc}`) + logger.error(error.stack) + break a } } } @@ -303,23 +271,16 @@ class PluginsLoader { /** 过滤事件 */ filtEvent(e, v) { if (!v.event) return false - let event = v.event.split(".") - let eventMap = { - message: ["post_type", "message_type", "sub_type"], - notice: ["post_type", "notice_type", "sub_type"], - request: ["post_type", "request_type", "sub_type"] + const event = v.event.split(".") + const eventMap = this.eventMap[e.post_type] || [] + const newEvent = [] + for (const i in event) { + if (event[i] == "*") + newEvent.push(event[i]) + else + newEvent.push(e[eventMap[i]]) } - let newEvent = [] - event.forEach((val, index) => { - if (val === "*") { - newEvent.push(val) - } else if (eventMap[e.post_type]) { - newEvent.push(e[eventMap[e.post_type][index]]) - } - }) - newEvent = newEvent.join(".") - - return v.event === newEvent + return v.event == newEvent.join(".") } /** 判断权限 */ @@ -336,10 +297,6 @@ class PluginsLoader { } if (e.isGroup) { - if (!e.member?._info) { - e.reply("数据加载中,请稍后再试") - return false - } if (v.permission == "owner") { if (!e.member.is_owner) { e.reply("暂无权限,只有群主才能操作") @@ -357,20 +314,17 @@ class PluginsLoader { return true } - dealEvent(e) { - if (!e.friend && e.user_id) e.friend = e.bot.pickFriend(e.user_id) - if (!e.group && e.group_id) e.group = e.bot.pickGroup(e.group_id) - if (!e.member && e.group && e.user_id) e.member = e.group.pickMember(e.user_id) - for (const i of [e.friend, e.group, e.member]) { - if (typeof i != "object") continue - if (!i.makeForwardMsg) i.makeForwardMsg = Bot.makeForwardMsg - if (!i.sendForwardMsg) i.sendForwardMsg = msg => Bot.sendForwardMsg(msg => i.sendMsg(msg), msg) - if (!i.getInfo) i.getInfo = () => i - } + dealText(text = "") { + if (cfg.bot["/→#"]) + text = text.replace(/^\s*\/\s*/, "#") + return text + .replace(/^\s*[#井]\s*/, "#") + .replace(/^\s*[*※]\s*/, "*") + .trim() } /** - * 处理消息,加入自定义字段 + * 处理事件,加入自定义字段 * @param e.msg 文本消息,多行会自动拼接 * @param e.img 图片消息数组 * @param e.atBot 是否at机器人 @@ -382,12 +336,11 @@ class PluginsLoader { * @param e.logText 日志用户字符串 * @param e.logFnc 日志方法字符串 */ - dealMsg(e) { + dealEvent(e) { if (e.message) for (const i of e.message) { switch (i.type) { case "text": - if (!e.msg) e.msg = "" - if (i.text) e.msg += i.text.replace(/^\s*[##井]+\s*/, "#").replace(/^\s*[\\**※]+\s*/, "*").trim() + e.msg = (e.msg || "") + this.dealText(i.text) break case "image": if (Array.isArray(e.img)) @@ -411,6 +364,10 @@ class PluginsLoader { case "file": e.file = i break + case "xml": + case "json": + e.msg = (e.msg || "") + (typeof i.data == "string" ? i.data : JSON.stringify(i.data)) + break } } @@ -423,37 +380,31 @@ class PluginsLoader { e.sender.card = e.sender.nickname } else { e.sender = { + user_id: e.user_id, + nickname: e.friend?.nickname, card: e.friend?.nickname, - nickname: e.friend?.nickname } } e.logText = `[${e.sender?.nickname ? `${e.sender.nickname}(${e.user_id})` : e.user_id}]` - } - - if (e.message_type == "group" || e.notice_type == "group") { + } else if (e.message_type == "group" || e.notice_type == "group") { e.isGroup = true + if (e.sender) { - e.sender.card = e.sender.card || e.sender.nickname - } else if (e.member) { - e.sender = { - card: e.member.card || e.member.nickname - } - } else if (e.nickname) { - e.sender = { - card: e.nickname, - nickname: e.nickname - } + if (!e.sender.card) + e.sender.card = e.sender.nickname } else { e.sender = { - card: "", - nickname: "" + user_id: e.user_id, + nickname: e.member?.nickname || e.friend?.nickname, + card: e.member?.card || e.member?.nickname || e.friend?.nickname, } } - if (!e.group_name) e.group_name = e.group?.name + if (!e.group_name && e.group?.name) + e.group_name = e.group.name - e.logText = `[${e.group_name ? `${e.group_name}(${e.group_id})` : e.group_id}, ${e.sender?.nickname ? `${e.sender.nickname}(${e.user_id})` : e.user_id}]` + e.logText = `[${e.group_name ? `${e.group_name}(${e.group_id})` : e.group_id}, ${e.sender?.card ? `${e.sender.card}(${e.user_id})` : e.user_id}]` } if (e.user_id && cfg.master[e.self_id]?.includes(String(e.user_id))) { @@ -479,24 +430,19 @@ class PluginsLoader { /** 处理回复,捕获发送失败异常 */ reply(e) { - if (e.reply) - e.replyNew = e.reply - else - e.replyNew = msg => { - if (e.isGroup) { - if (e.group?.sendMsg) { - return e.group.sendMsg(msg) - } else { - return e.bot.pickGroup(e.group_id).sendMsg(msg) - } - } else { - if (e.friend?.sendMsg) { - return e.friend.sendMsg(msg) - } else { - return e.bot.pickFriend(e.user_id).sendMsg(msg) - } - } + const reply = e.reply ? e.reply.bind(e) : msg => { + if (e.isGroup) { + if (e.group?.sendMsg) + return e.group.sendMsg(msg) + else + return e.bot.pickGroup(e.group_id).sendMsg(msg) + } else { + if (e.friend?.sendMsg) + return e.friend.sendMsg(msg) + else + return e.bot.pickFriend(e.user_id).sendMsg(msg) } + } /** * @param msg 发送的消息 @@ -509,13 +455,13 @@ class PluginsLoader { let { recallMsg = 0, at = "" } = data - if (at) { + if (at && e.isGroup) { if (at === true) at = e.user_id if (Array.isArray(msg)) - msg.unshift(segment.at(at)) + msg.unshift(segment.at(at), "\n") else - msg = [segment.at(at), msg] + msg = [segment.at(at), "\n", msg] } if (quote && e.message_id) { @@ -527,10 +473,9 @@ class PluginsLoader { let res try { - res = await e.replyNew(msg) + res = await reply(msg) } catch (err) { - Bot.makeLog("error", `发送消息错误:${Bot.String(msg)}`, e.self_id) - logger.error(err) + Bot.makeLog("error", ["发送消息错误", msg, err], e.self_id) } if (recallMsg > 0 && res?.message_id) { @@ -548,93 +493,63 @@ class PluginsLoader { }, recallMsg * 1000) } - this.count(e, msg) + this.count(e, "send", msg) return res } } - count(e, msg) { - let screenshot = false - if (msg && msg?.file) - screenshot = true - - this.saveCount("sendMsg") - if (screenshot) - this.saveCount("screenshot") - - if (e.group_id) { - this.saveCount("sendMsg", e.group_id) - if (screenshot) - this.saveCount("screenshot", e.group_id) - } + async count(e, type, msg) { + if (cfg.bot.msg_type_count) + for (const i of Array.isArray(msg) ? msg : [msg]) + await this.saveCount(e, `${type}:${i?.type || "text"}`) + await this.saveCount(e, `${type}:msg`) } - saveCount(type, groupId = "") { - let key = "Yz:count:" - - if (groupId) { - key += `group:${groupId}:` + async saveCount(e, type) { + const key = [] + + const day = moment().format("YYYY:MM:DD") + const month = moment().format("YYYY:MM") + const year = moment().format("YYYY") + for (const i of [day, month, year, "total"]) { + key.push(`total:${i}`) + if (e.self_id) key.push(`bot:${e.self_id}:${i}`) + if (e.user_id) key.push(`user:${e.user_id}:${i}`) + if (e.group_id) key.push(`group:${e.group_id}:${i}`) } - let dayKey = `${key}${type}:day:${moment().format("MMDD")}` - let monthKey = `${key}${type}:month:${Number(moment().month()) + 1}` - let totalKey = `${key}${type}:total` - - redis.incr(dayKey) - redis.incr(monthKey) - if (!groupId) redis.incr(totalKey) - redis.expire(dayKey, 3600 * 24 * 30) - redis.expire(monthKey, 3600 * 24 * 30) - } - - delCount() { - let key = "Yz:count:" - redis.set(`${key}sendMsg:total`, "0") - redis.set(`${key}screenshot:total`, "0") + for (const i of key) + await redis.incr(`Yz:count:${type}:${i}`) } /** 收集定时任务 */ collectTask(task) { - if (Array.isArray(task)) { - task.forEach((val) => { - if (!val.cron) return - if (!val.name) throw new Error("插件任务名称错误") - this.task.push(val) - }) - } else { - if (task.fnc && task.cron) { - if (!task.name) throw new Error("插件任务名称错误") - this.task.push(task) - } - } + for (const i of Array.isArray(task) ? task : [task]) + if (i.cron && i.name) + this.task.push(i) } /** 创建定时任务 */ - creatTask() { - if (process.argv[1].includes("test")) return - this.task.forEach((val) => { - val.job = schedule.scheduleJob(val.cron, async () => { + createTask() { + for (const i of this.task) + i.job = schedule.scheduleJob(i.cron, async () => { try { - if (val.log === true) { - logger.mark(`开始定时任务:${val.name}`) - } - let res = val.fnc() - if (util.types.isPromise(res)) res = await res - if (val.log === true) { - logger.mark(`定时任务完成:${val.name}`) - } + if (i.log == true) + logger.mark(`开始定时任务:${i.name}`) + await i.fnc() + if (i.log == true) + logger.mark(`定时任务完成:${i.name}`) } catch (error) { - logger.error(`定时任务报错:${val.name}`) + logger.error(`定时任务报错:${i.name}`) logger.error(error) } }) - }) } /** 检查命令冷却cd */ checkLimit(e) { /** 禁言中 */ - if (e.isGroup && e?.group?.mute_left > 0) return false + if (e.isGroup && e.group?.mute_left > 0) return false if (!e.message || e.isPrivate) return true const config = cfg.getGroup(e.self_id, e.group_id) @@ -661,16 +576,12 @@ class PluginsLoader { if (config.groupCD) { this.groupCD[e.group_id] = true - setTimeout(() => { - delete this.groupCD[e.group_id] - }, config.groupCD) + setTimeout(() => delete this.groupCD[e.group_id], config.groupCD) } if (config.singleCD) { - let key = `${e.group_id}.${e.user_id}` + const key = `${e.group_id}.${e.user_id}` this.singleCD[key] = true - setTimeout(() => { - delete this.singleCD[key] - }, config.singleCD) + setTimeout(() => delete this.singleCD[key], config.singleCD) } } @@ -680,7 +591,11 @@ class PluginsLoader { let groupCfg = cfg.getGroup(e.self_id, e.group_id) - if (groupCfg.onlyReplyAt != 1 || !groupCfg.botAlias) return true + /** 模式0,未开启前缀 */ + if (groupCfg.onlyReplyAt == 0 || !groupCfg.botAlias) return true + + /** 模式2,非主人开启 */ + if (groupCfg.onlyReplyAt == 2 && e.isMaster) return true /** at机器人 */ if (e.atBot) return true @@ -693,50 +608,54 @@ class PluginsLoader { /** 判断黑白名单 */ checkBlack(e) { - let other = cfg.getOther() - - if (e.test) return true + const other = cfg.getOther() - /** 黑名单qq */ - if (other.blackQQ?.length && other.blackQQ.includes(Number(e.user_id) || String(e.user_id))) { + /** 黑名单用户 */ + if (other.blackUser?.length && other.blackUser.includes(Number(e.user_id) || String(e.user_id))) + return false + /** 白名单用户 */ + if (other.whiteUser?.length && !other.whiteUser.includes(Number(e.user_id) || String(e.user_id))) return false - } if (e.group_id) { - /** 白名单群 */ - if (other.whiteGroup?.length) { - if (other.whiteGroup.includes(Number(e.group_id) || String(e.group_id))) return true - return false - } /** 黑名单群 */ - if (other.blackGroup?.length && other.blackGroup.includes(Number(e.group_id) || String(e.group_id))) { + if (other.blackGroup?.length && other.blackGroup.includes(Number(e.group_id) || String(e.group_id))) + return false + /** 白名单群 */ + if (other.whiteGroup?.length && !other.whiteGroup.includes(Number(e.group_id) || String(e.group_id))) return false - } } return true } /** 判断是否启用功能 */ - checkDisable(e, p) { - let groupCfg = cfg.getGroup(e.self_id, e.group_id) - if (!lodash.isEmpty(groupCfg.enable)) { - if (groupCfg.enable.includes(p.name)) { - return true - } - // logger.debug(`${e.logText}[${p.name}]功能已禁用`) + checkDisable(p) { + const groupCfg = cfg.getGroup(p.e.self_id, p.e.group_id) + if (groupCfg.disable?.length && groupCfg.disable.includes(p.name)) return false - } - - if (!lodash.isEmpty(groupCfg.disable)) { - if (groupCfg.disable.includes(p.name)) { - // logger.debug(`${e.logText}[${p.name}]功能已禁用`) - return false - } + if (groupCfg.enable?.length && !groupCfg.enable.includes(p.name)) + return false + return true + } - return true + async changePlugin(key) { + try { + let app = await import(`../../${this.dir}/${key}?${moment().format("x")}`) + if (app.apps) app = { ...app.apps } + lodash.forEach(app, p => { + const plugin = new p + for (const i in this.priority) + if (this.priority[i].key == key && this.priority[i].name == plugin.name) { + this.priority[i].class = p + this.priority[i].priority = plugin.priority + } + }) + this.priority = lodash.orderBy(this.priority, ["priority"], ["asc"]) + } catch (error) { + logger.error(`加载插件错误:${logger.red(key)}`) + logger.error(decodeURI(error.stack)) } - return true } /** 监听热更新 */ @@ -744,129 +663,50 @@ class PluginsLoader { this.watchDir(dirName) if (this.watcher[`${dirName}.${appName}`]) return - let file = `./plugins/${dirName}/${appName}` + const file = `./${this.dir}/${dirName}/${appName}` const watcher = chokidar.watch(file) - let key = `${dirName}/${appName}` + const key = `${dirName}/${appName}` /** 监听修改 */ - watcher.on("change", async path => { + watcher.on("change", path => { logger.mark(`[修改插件][${dirName}][${appName}]`) - - let tmp = {} - try { - tmp = await import(`../../plugins/${dirName}/${appName}?${moment().format("x")}`) - } catch (error) { - logger.error(`载入插件错误:${logger.red(dirName + "/" + appName)}`) - logger.error(decodeURI(error.stack)) - return - } - - if (tmp.apps) tmp = { ...tmp.apps } - lodash.forEach(tmp, (p) => { - /* eslint-disable new-cap */ - let plugin = new p() - for (let i in this.priority) { - if (this.priority[i].key == key) { - this.priority[i].class = p - this.priority[i].priority = plugin.priority - } - } - - if (plugin.handler) { - lodash.forEach(plugin.handler, ({ fn, key, priority }) => { - Handler.add({ - ns: plugin.namespace || File.name, - key: key, - self: plugin, - property: priority || plugin.priority || 500, - fn: plugin[fn] - }) - }) - } - }) - - this.priority = lodash.orderBy(this.priority, ["priority"], ["asc"]) + this.changePlugin(key) }) /** 监听删除 */ watcher.on("unlink", async path => { logger.mark(`[卸载插件][${dirName}][${appName}]`) - for (let i in this.priority) { - if (this.priority[i].key == key) { + /** 停止更新监听 */ + this.watcher[`${dirName}.${appName}`].removeAllListeners("change") + for (const i in this.priority) + if (this.priority[i].key == key) this.priority.splice(i, 1) - /** 停止更新监听 */ - this.watcher[`${dirName}.${appName}`].removeAllListeners("change") - break - } - } }) - this.watcher[`${dirName}.${appName}`] = watcher } /** 监听文件夹更新 */ watchDir(dirName) { if (this.watcher[dirName]) return - - let file = `./plugins/${dirName}/` - const watcher = chokidar.watch(file) - + const watcher = chokidar.watch(`./${this.dir}/${dirName}/`) /** 热更新 */ - setTimeout(() => { + Bot.once("online", () => { /** 新增文件 */ watcher.on("add", async PluPath => { - let appName = path.basename(PluPath) + const appName = path.basename(PluPath) if (!appName.endsWith(".js")) return - if (!fs.existsSync(`${this.dir}/${dirName}/${appName}`)) return - - let key = `${dirName}/${appName}` - - this.watch(dirName, appName) - - /** 太快了延迟下 */ - await common.sleep(500) - logger.mark(`[新增插件][${dirName}][${appName}]`) - let tmp = {} - try { - tmp = await import(`../../plugins/${dirName}/${appName}?${moment().format("X")}`) - } catch (error) { - logger.error(`载入插件错误:${logger.red(dirName + "/" + appName)}`) - logger.error(decodeURI(error.stack)) - return - } - - if (tmp.apps) tmp = { ...tmp.apps } - - lodash.forEach(tmp, (p) => { - if (!p.prototype) { - logger.error(`[载入失败][${dirName}][${appName}] 格式错误已跳过`) - return - } - /* eslint-disable new-cap */ - let plugin = new p() - - for (let i in this.priority) { - if (this.priority[i].key == key) { - return - } - } - - this.priority.push({ - class: p, - key, - name: plugin.name, - priority: plugin.priority - }) + const key = `${dirName}/${appName}` + await this.importPlugin({ + name: key, + path: `../../${this.dir}/${key}?${moment().format("X")}`, }) - /** 优先级排序 */ this.priority = lodash.orderBy(this.priority, ["priority"], ["asc"]) + this.watch(dirName, appName) }) - }, 500) - + }) this.watcher[dirName] = watcher } } - export default new PluginsLoader() \ No newline at end of file diff --git a/Yunzai/lib/plugins/plugin.js b/Yunzai/lib/plugins/plugin.js index 3e738da10c7f81d9109d54afd40ae119c8ec9326..15183d107831b8f3b7f50354a3cba3a69cff29d3 100644 --- a/Yunzai/lib/plugins/plugin.js +++ b/Yunzai/lib/plugins/plugin.js @@ -1,6 +1,9 @@ -import { Common } from '#miao' +let Common +try { + Common = (await import("#miao")).Common +} catch (err) {} -let stateArr = {} +const stateArr = {} export default class plugin { /** @@ -24,16 +27,16 @@ export default class plugin { * @param task.fnc 定时任务方法名 * @param task.log false时不显示执行日志 */ - constructor ({ - name = 'your-plugin', - dsc = '无', - handler, - namespace, - event = 'message', - priority = 5000, - task = { fnc: '', cron: '' }, - rule = [] - }) { + constructor({ + name = "your-plugin", + dsc = "无", + handler, + namespace, + event = "message", + priority = 5000, + task = { fnc: "", cron: "" }, + rule = [] + }) { /** 插件名称 */ this.name = name /** 插件描述 */ @@ -45,18 +48,18 @@ export default class plugin { /** 定时任务,可以是数组 */ this.task = { /** 任务名 */ - name: '', + name: "", /** 任务方法名 */ - fnc: task.fnc || '', + fnc: task.fnc || "", /** 任务cron表达式 */ - cron: task.cron || '' + cron: task.cron || "" } /** 命令规则 */ this.rule = rule if (handler) { this.handler = handler - this.namespace = namespace || '' + this.namespace = namespace || "" } } @@ -66,12 +69,12 @@ export default class plugin { * @param data.recallMsg 群聊是否撤回消息,0-120秒,0不撤回 * @param data.at 是否at用户 */ - reply (msg = '', quote = false, data = {}) { - if (!this.e.reply || !msg) return false + reply(msg = "", quote = false, data = {}) { + if (!this.e?.reply || !msg) return false return this.e.reply(msg, quote, data) } - conKey (isGroup = false) { + conKey(isGroup = false) { if (isGroup) { return `${this.name}.${this.e.group_id}` } else { @@ -84,45 +87,36 @@ export default class plugin { * @param isGroup 是否群聊 * @param time 操作时间,默认120秒 */ - setContext (type, isGroup = false, time = 120) { - let key = this.conKey(isGroup) + setContext(type, isGroup, time = 120) { + const key = this.conKey(isGroup) if (!stateArr[key]) stateArr[key] = {} stateArr[key][type] = this.e - if (time) { - /** 操作时间 */ - setTimeout(() => { - if (stateArr[key][type]) { - delete stateArr[key][type] - this.e.reply('操作超时已取消', true) - } - }, time * 1000) - } - } - - getContext () { - let key = this.conKey() - return stateArr[key] + if (time) stateArr[key][type].timeout = setTimeout(() => { + if (stateArr[key][type]) { + delete stateArr[key][type] + this.reply("操作超时已取消", true) + } + }, time * 1000) } - getContextGroup () { - let key = this.conKey(true) - return stateArr[key] + getContext(type, isGroup) { + if (type) return stateArr[this.conKey(isGroup)]?.[type] + return stateArr[this.conKey(isGroup)] } /** * @param type 执行方法 * @param isGroup 是否群聊 */ - finish (type, isGroup = false) { - if (stateArr[this.conKey(isGroup)] && stateArr[this.conKey(isGroup)][type]) { - delete stateArr[this.conKey(isGroup)][type] + finish(type, isGroup) { + const key = this.conKey(isGroup) + if (stateArr[key] && stateArr[key][type]) { + clearTimeout(stateArr[key][type].timeout) + delete stateArr[key][type] } } - async renderImg (plugin, tpl, data, cfg) { - return Common.render(plugin, tpl, data, { - ...cfg, - e: this.e - }) + async renderImg(plugin, tpl, data, cfg) { + return Common.render(plugin, tpl, data, { ...cfg, e: this.e }) } -} +} \ No newline at end of file diff --git a/Yunzai/lib/plugins/runtime.js b/Yunzai/lib/plugins/runtime.js index ae912ac725e290b2173835b803561af0adc141d6..40e77e0265255b7230f142cb2d8a201621fe774c 100644 --- a/Yunzai/lib/plugins/runtime.js +++ b/Yunzai/lib/plugins/runtime.js @@ -4,25 +4,29 @@ * 提供一些常用的运行时变量、方法及model获取 * 降低对目录结构的依赖 */ -import lodash from 'lodash' -import fs from 'node:fs' -import gsCfg from '../../plugins/genshin/model/gsCfg.js' -import common from '../common/common.js' -import cfg from '../config/config.js' -import MysApi from '../../plugins/genshin/model/mys/mysApi.js' -import MysInfo from '../../plugins/genshin/model/mys/mysInfo.js' -import puppeteer from '../puppeteer/puppeteer.js' -import { Version } from '#miao' -import NoteUser from '../../plugins/genshin/model/mys/NoteUser.js' -import MysUser from '../../plugins/genshin/model/mys/MysUser.js' -import Handler from './handler.js' +import lodash from "lodash" +import fs from "node:fs/promises" +import common from "../common/common.js" +import cfg from "../config/config.js" +import puppeteer from "../puppeteer/puppeteer.js" +import Handler from "./handler.js" + +let gsCfg, MysApi, MysInfo, NoteUser, MysUser, Version +try { + gsCfg = (await import("../../plugins/genshin/model/gsCfg.js")).default + MysApi = (await import("../../plugins/genshin/model/mys/mysApi.js")).default + MysInfo = (await import("../../plugins/genshin/model/mys/mysInfo.js")).default + NoteUser = (await import("../../plugins/genshin/model/mys/NoteUser.js")).default + MysUser = (await import("../../plugins/genshin/model/mys/MysUser.js")).default + Version = (await import("#miao")).Version +} catch (err) {} /** * 常用的处理方法 */ export default class Runtime { - constructor (e) { + constructor(e) { this.e = e this._mysInfo = {} @@ -33,55 +37,54 @@ export default class Runtime { } } - get uid () { + get uid() { return this.user?.uid } - get hasCk () { + get hasCk() { return this.user?.hasCk } - get user () { + get user() { return this.e.user } - get cfg () { + get cfg() { return cfg } - get gsCfg () { + get gsCfg() { return gsCfg } - get common () { + get common() { return common } - get puppeteer () { + get puppeteer() { return puppeteer } - get MysInfo () { + get MysInfo() { return MysInfo } - get NoteUser () { + get NoteUser() { return NoteUser } - get MysUser () { + get MysUser() { return MysUser } - static async init (e) { - await MysInfo.initCache() - let runtime = new Runtime(e) - e.runtime = runtime - await runtime.initUser() - return runtime + static async init(e) { + if (MysInfo) await MysInfo.initCache() + e.runtime = new Runtime(e) + if (NoteUser) await e.runtime.initUser() + return e.runtime } - async initUser () { + async initUser() { let e = this.e let user = await NoteUser.create(e) if (user) { @@ -89,24 +92,24 @@ export default class Runtime { get (self, key, receiver) { let game = e.game let fnMap = { - uid: 'getUid', - uidList: 'getUidList', - mysUser: 'getMysUser', - ckUidList: 'getCkUidList' + uid: "getUid", + uidList: "getUidList", + mysUser: "getMysUser", + ckUidList: "getCkUidList" } if (fnMap[key]) { return self[fnMap[key]](game) } - if (key === 'uidData') { - return self.getUidData('', game) + if (key === "uidData") { + return self.getUidData("", game) } - if (['getUid', 'getUidList', 'getMysUser', 'getCkUidList', 'getUidMapList', 'getGameDs'].includes(key)) { + if (["getUid", "getUidList", "getMysUser", "getCkUidList", "getUidMapList", "getGameDs"].includes(key)) { return (_game, arg2) => { return self[key](_game || game, arg2) } } - if (['getUidData', 'hasUid', 'addRegUid', 'delRegUid', 'setMainUid'].includes(key)) { - return (uid, _game = '') => { + if (["getUidData", "hasUid", "addRegUid", "delRegUid", "setMainUid"].includes(key)) { + return (uid, _game = "") => { return self[key](uid, _game || game) } } @@ -122,14 +125,14 @@ export default class Runtime { * @param targetType all: 所有用户均可, cookie:查询用户必须具备Cookie * @returns {Promise} */ - async getMysInfo (targetType = 'all') { + async getMysInfo(targetType = "all") { if (!this._mysInfo[targetType]) { - this._mysInfo[targetType] = await MysInfo.init(this.e, targetType === 'cookie' ? 'detail' : 'roleIndex') + this._mysInfo[targetType] = await MysInfo.init(this.e, targetType === "cookie" ? "detail" : "roleIndex") } return this._mysInfo[targetType] } - async getUid () { + async getUid() { return await MysInfo.getUid(this.e) } @@ -140,7 +143,7 @@ export default class Runtime { * @param option MysApi option * @returns {Promise} */ - async getMysApi (targetType = 'all', option = {}) { + async getMysApi(targetType = "all", option = {}) { let mys = await this.getMysInfo(targetType) if (mys.uid && mys?.ckInfo?.ck) { return new MysApi(mys.uid, mys.ckInfo.ck, option) @@ -155,7 +158,7 @@ export default class Runtime { * @param option * @returns {Promise} */ - async createMysApi (uid, ck, option) { + async createMysApi(uid, ck, option) { return new MysApi(uid, ck, option) } @@ -172,27 +175,17 @@ export default class Runtime { * @param cfg.beforeRender({data}) 可改写渲染的data数据 * @returns {Promise} */ - async render (plugin, path, data = {}, cfg = {}) { + async render(plugin, path, data = {}, cfg = {}) { // 处理传入的path - path = path.replace(/.html$/, '') - let paths = lodash.filter(path.split('/'), (p) => !!p) - path = paths.join('/') + path = path.replace(/.html$/, "") + let paths = lodash.filter(path.split("/"), (p) => !!p) + path = paths.join("/") // 创建目录 - const mkdir = (check) => { - let currDir = `${process.cwd()}/temp` - for (let p of check.split('/')) { - currDir = `${currDir}/${p}` - if (!fs.existsSync(currDir)) { - fs.mkdirSync(currDir) - } - } - return currDir - } - mkdir(`html/${plugin}/${path}`) + await Bot.mkdir(`temp/html/${plugin}/${path}`) // 自动计算pluResPath - let pluResPath = `../../../${lodash.repeat('../', paths.length)}plugins/${plugin}/resources/` - let miaoResPath = `../../../${lodash.repeat('../', paths.length)}plugins/miao-plugin/resources/` - const layoutPath = process.cwd() + '/plugins/miao-plugin/resources/common/layout/' + let pluResPath = `../../../${lodash.repeat("../", paths.length)}plugins/${plugin}/resources/` + let miaoResPath = `../../../${lodash.repeat("../", paths.length)}plugins/miao-plugin/resources/` + const layoutPath = process.cwd() + "/plugins/miao-plugin/resources/common/layout/" // 渲染data data = { sys: { @@ -202,9 +195,9 @@ export default class Runtime { copyright: `Created By TRSS-Yunzai${Version.yunzai} `, _res_path: pluResPath, _miao_path: miaoResPath, - _tpl_path: process.cwd() + '/plugins/miao-plugin/resources/common/tpl/', - defaultLayout: layoutPath + 'default.html', - elemLayout: layoutPath + 'elem.html', + _tpl_path: process.cwd() + "/plugins/miao-plugin/resources/common/tpl/", + defaultLayout: layoutPath + "default.html", + elemLayout: layoutPath + "elem.html", ...data, @@ -215,7 +208,7 @@ export default class Runtime { tplFile: `./plugins/${plugin}/resources/${path}.html`, saveId: data.saveId || data.save_id || paths[paths.length - 1], pageGotoParams: { - waitUntil: 'networkidle2' + waitUntil: "networkidle2" } } // 处理beforeRender @@ -223,16 +216,16 @@ export default class Runtime { data = cfg.beforeRender({ data }) || data } // 保存模板数据 - if (process.argv.includes('dev')) { + if (process.argv.includes("dev")) { // debug下保存当前页面的渲染数据,方便模板编写与调试 // 由于只用于调试,开发者只关注自己当时开发的文件即可,暂不考虑app及plugin的命名冲突 - let saveDir = mkdir(`ViewData/${plugin}`) - let file = `${saveDir}/${data._htmlPath.split('/').join('_')}.json` - fs.writeFileSync(file, JSON.stringify(data)) + let saveDir = await Bot.mkdir(`temp/ViewData/${plugin}`) + let file = `${saveDir}/${data._htmlPath.split("/").join("_")}.json` + await fs.writeFile(file, JSON.stringify(data)) } // 截图 let base64 = await puppeteer.screenshot(`${plugin}/${path}`, data) - if (cfg.retType === 'base64') { + if (cfg.retType === "base64") { return base64 } let ret = true @@ -243,6 +236,6 @@ export default class Runtime { ret = await this.e.reply(base64) } } - return cfg.retType === 'msgId' ? ret : true + return cfg.retType === "msgId" ? ret : true } } diff --git a/Yunzai/lib/plugins/stdin.js b/Yunzai/lib/plugins/stdin.js index b833d87cc5e99a3f1da98b4e5b4e74ae153ddd58..fad03f37d39544aae987ecf44c421b6f3f1ad8fe 100644 --- a/Yunzai/lib/plugins/stdin.js +++ b/Yunzai/lib/plugins/stdin.js @@ -1,13 +1,11 @@ -import fs from "node:fs" +import fs from "node:fs/promises" import path from "node:path" -import common from "../common/common.js" Bot.adapter.push(new class stdinAdapter { constructor() { this.id = "stdin" this.name = "标准输入" this.path = "data/stdin/" - common.mkdirs(this.path) } async sendMsg(msg) { @@ -19,10 +17,10 @@ Bot.adapter.push(new class stdinAdapter { let file if (i.file) { - file = await Bot.fileType(i.file, i.name) + file = await Bot.fileType(i) if (Buffer.isBuffer(file.buffer)) { file.path = `${this.path}${file.name || Date.now()}` - fs.writeFileSync(file.path, file.buffer) + await fs.writeFile(file.path, file.buffer) } } @@ -71,7 +69,7 @@ Bot.adapter.push(new class stdinAdapter { const files = `${this.path}${Date.now()}-${name}` logger.info(`${logger.blue(`[${this.id}]`)} 发送文件:${file}\n文件已保存到:${logger.cyan(files)}`) - return fs.writeFileSync(files, buffer) + return fs.writeFile(files, buffer) } pickFriend() { @@ -103,14 +101,15 @@ Bot.adapter.push(new class stdinAdapter { Bot.em(`${data.post_type}.${data.message_type}`, data) } - load() { + async load() { + await Bot.mkdir(this.path) Bot[this.id] = { adapter: this, uin: this.id, nickname: this.name, - stat: { start_time: Date.now()/1000 }, version: { id: this.id, name: this.name }, pickFriend: () => this.pickFriend(), + get stat() { return Bot.stat }, get pickUser() { return this.pickFriend }, get pickMember() { return this.pickFriend }, get pickGroup() { return this.pickFriend }, diff --git a/Yunzai/lib/renderer/loader.js b/Yunzai/lib/renderer/loader.js index f7506119c70c1171bd9e82a74772f7234f419d7d..129fe6144ef83fceaa3494c2f3b4c5f38c1babd2 100644 --- a/Yunzai/lib/renderer/loader.js +++ b/Yunzai/lib/renderer/loader.js @@ -1,9 +1,13 @@ -import fs from 'node:fs' -import yaml from 'yaml' -import lodash from 'lodash' -import cfg from '../config/config.js' -import { Data } from '#miao' -import Renderer from './Renderer.js' +import fs from "node:fs" +import yaml from "yaml" +import lodash from "lodash" +import cfg from "../config/config.js" +import Renderer from "./Renderer.js" + +let Data +try { + Data = (await import("#miao")).Data +} catch (err) {} /** 全局变量 Renderer */ global.Renderer = Renderer @@ -14,7 +18,7 @@ global.Renderer = Renderer class RendererLoader { constructor() { this.renderers = new Map() - this.dir = './renderers' + this.dir = "./renderers" // TODO 渲染器热加载 this.watcher = {} } @@ -26,16 +30,17 @@ class RendererLoader { } async load() { + if (!Data) return const subFolders = fs.readdirSync(this.dir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()) for (let subFolder of subFolders) { let name = subFolder.name try { const rendererFn = await Data.importDefault(`${this.dir}/${name}/index.js`) let configFile = `${this.dir}/${name}/config.yaml` - let rendererCfg = fs.existsSync(configFile) ? yaml.parse(fs.readFileSync(configFile, 'utf8')) : {} + let rendererCfg = fs.existsSync(configFile) ? yaml.parse(fs.readFileSync(configFile, "utf8")) : {} let renderer = rendererFn(rendererCfg) if (!renderer.id || !renderer.type || !renderer.render || !lodash.isFunction(renderer.render)) { - logger.warn('渲染后端 ' + (renderer.id || subFolder.name) + ' 不可用') + logger.warn("渲染后端 " + (renderer.id || subFolder.name) + " 不可用") } this.renderers.set(renderer.id, renderer) logger.info(`加载渲染后端 ${renderer.id}`) @@ -46,9 +51,9 @@ class RendererLoader { } } - getRenderer(name = cfg.renderer?.name || 'puppeteer') { + getRenderer(name = cfg.renderer?.name || "puppeteer") { // TODO 渲染器降级 - return this.renderers.get(name) + return this.renderers.get(name) || {} } } diff --git a/Yunzai/lib/tools/web.js b/Yunzai/lib/tools/web.js index 32ff156fdfc500239352f888c3da06d4421c562d..890e7adaa289004344c4939d6277e4d9d7b090ba 100644 --- a/Yunzai/lib/tools/web.js +++ b/Yunzai/lib/tools/web.js @@ -1,7 +1,7 @@ -import express from 'express' -import template from 'express-art-template' -import fs from 'fs' -import lodash from 'lodash' +import express from "express" +import template from "express-art-template" +import fs from "node:fs/promises" +import lodash from "lodash" /* * npm run app web-debug开启Bot后 @@ -17,53 +17,53 @@ let app = express() let _path = process.cwd() -app.engine('html', template) -app.set('views', _path + '/resources/') -app.set('view engine', 'art') -app.use(express.static(_path + '/resources')) -app.use('/plugins', express.static('plugins')) +app.engine("html", template) +app.set("views", _path + "/resources/") +app.set("view engine", "art") +app.use(express.static(_path + "/resources")) +app.use("/plugins", express.static("plugins")) -app.get('/', function (req, res) { - let pluginList = fs.readdirSync(_path + '/temp/ViewData/') || [] +app.get("/", async function (req, res) { + let pluginList = await fs.readdir(_path + "/temp/ViewData/") || [] let html = [ - '在npm run web-dev模式下触发截图消息后,可在下方选择页面进行调试', - '如果页面内资源路径不正确请使用{{_res_path}}作为根路径,对应之前的../../../../', - '可直接修改模板html或css刷新查看效果' + "在npm run web-dev模式下触发截图消息后,可在下方选择页面进行调试", + "如果页面内资源路径不正确请使用{{_res_path}}作为根路径,对应之前的../../../../", + "可直接修改模板html或css刷新查看效果" ] let li = {} for (let pIdx in pluginList) { const plugin = pluginList[pIdx] - let fileList = fs.readdirSync(_path + `/temp/ViewData/${plugin}/`) || [] + let fileList = await fs.readdir(_path + `/temp/ViewData/${plugin}/`) || [] for (let idx in fileList) { let ret = /(.+)\.json$/.exec(fileList[idx]) if (ret && ret[1]) { - let text = [plugin, ...ret[1].split('_')] - li[text.join('')] = (`
  • ${text.join(' / ')}
  • `) + let text = [plugin, ...ret[1].split("_")] + li[text.join("")] = (`
  • ${text.join(" / ")}
  • `) } } } - res.send(html.join('
    ') + '
      ' + lodash.values(li).join('') + '
    ') + res.send(html.join("
    ") + "
      " + lodash.values(li).join("") + "
    ") }) -app.get('/:page', function (req, res) { - let [plugin, app, ...page] = req.params.page.split('_') - page = page.join('_') - if (plugin == 'favicon.ico') { - return res.send('') +app.get("/:page", async function (req, res) { + let [plugin, app, ...page] = req.params.page.split("_") + page = page.join("_") + if (plugin == "favicon.ico") { + return res.send("") } - let data = JSON.parse(fs.readFileSync(_path + `/temp/ViewData/${plugin}/${app}_${page}.json`, 'utf8')) + let data = JSON.parse(await fs.readFile(_path + `/temp/ViewData/${plugin}/${app}_${page}.json`, "utf8")) data = data || {} - data._res_path = '' + data._res_path = "" data._sys_res_path = data._res_path if (data._plugin) { data._res_path = `/plugins/${data._plugin}/resources/` data.pluResPath = data._res_path } - let htmlPath = '' + let htmlPath = "" let tplPath = `${app}/${htmlPath}${page}/${page}.html` if (data._plugin) { - tplPath = `../plugins/${data._plugin}/resources/${htmlPath}/${app}/${page.split('_').join('/')}.html` + tplPath = `../plugins/${data._plugin}/resources/${htmlPath}/${app}/${page.split("_").join("/")}.html` } else if (data._no_type_path) { tplPath = `${app}/${page}.html` } @@ -71,4 +71,4 @@ app.get('/:page', function (req, res) { }) app.listen(8000) -console.log('页面服务已启动,触发消息图片后访问 http://localhost:8000/ 调试页面') +console.log("页面服务已启动,触发消息图片后访问 http://localhost:8000/ 调试页面") diff --git a/Yunzai/package.json b/Yunzai/package.json index 014205becac877aa6205699b20f40c1317372995..9fe91fdf3dba709ec66537c81a9a005a0b051b69 100644 --- a/Yunzai/package.json +++ b/Yunzai/package.json @@ -8,41 +8,41 @@ "scripts": { "app": "node .", "dev": "node . dev", - "web": "node ./lib/tools/web.js", - "test": "node ./lib/tools/test.js", - "start": "pm2 start ./config/pm2/pm2.json", - "stop": "pm2 stop ./config/pm2/pm2.json", - "restart": "pm2 restart ./config/pm2/pm2.json", - "log": "node ./lib/tools/log.js" + "web": "node lib/tools/web.js", + "start": "pm2 start config/pm2.yaml", + "stop": "pm2 stop config/pm2.yaml", + "restart": "pm2 restart config/pm2.yaml", + "log": "pm2 log --lines 100" }, "dependencies": { "art-template": "^4.13.2", "chalk": "^5.3.0", - "chokidar": "^3.5.3", - "express": "^4.18.2", - "file-type": "^18.7.0", - "https-proxy-agent": "7.0.2", - "image-size": "^1.0.2", + "chokidar": "^3.6.0", + "express": "^4.19.1", + "file-type": "^19.0.0", + "https-proxy-agent": "7.0.4", + "image-size": "^1.1.1", + "level": "^8.0.1", "lodash": "^4.17.21", "log4js": "^6.9.1", - "md5": "^2.3.0", + "md5": "link:lib/modules/md5", "moment": "^2.30.1", - "node-fetch": "^3.3.2", + "node-fetch": "link:lib/modules/node-fetch", "node-schedule": "^2.1.1", "oicq": "link:lib/modules/oicq", - "pm2": "^5.3.0", - "puppeteer": "^21.6.1", - "redis": "^4.6.12", - "sequelize": "^6.35.2", - "sqlite3": "^5.1.6", + "pm2": "^5.3.1", + "puppeteer": "*", + "redis": "^4.6.13", + "sequelize": "^6.37.1", + "sqlite3": "5.1.6", "ws": "^8.16.0", - "yaml": "^2.3.4" + "yaml": "^2.4.1" }, "devDependencies": { - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-n": "^16.5.0", + "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1" }, "imports": { diff --git a/Yunzai/plugins/adapter/ComWeChat.js b/Yunzai/plugins/adapter/ComWeChat.js index a0b910c93434355b9f14d86fb45f222b6f6a7e31..ddc424820f2d3975f37457367ba851a74925e1d0 100644 --- a/Yunzai/plugins/adapter/ComWeChat.js +++ b/Yunzai/plugins/adapter/ComWeChat.js @@ -1,7 +1,5 @@ -import { randomUUID } from "crypto" +import { randomUUID } from "node:crypto" import path from "node:path" -import fs from "node:fs" -import { fileTypeFromBuffer } from "file-type" Bot.adapter.push(new class ComWeChatAdapter { constructor() { @@ -10,71 +8,38 @@ Bot.adapter.push(new class ComWeChatAdapter { this.path = this.name } - toStr(data) { - switch (typeof data) { - case "string": - return data - case "number": - return String(data) - case "object": - if (Buffer.isBuffer(data)) - return Buffer.from(data, "utf8").toString() - else - return JSON.stringify(data) - } - return data - } - makeLog(msg) { - return this.toStr(msg).replace(/(base64:\/\/|"type":"data","data":").*?"/g, '$1..."') + return Bot.String(msg).replace(/(base64:\/\/|"type":"data","data":").*?"/g, '$1..."') } sendApi(ws, action, params = {}) { const echo = randomUUID() - const msg = { action, params, echo } - ws.sendMsg(msg) - return new Promise(resolve => - Bot.once(echo, data => - resolve({ ...data, ...data.data }))) - } - - async fileName(file) { - try { - if (file.match(/^base64:\/\//)) { - const buffer = Buffer.from(file.replace(/^base64:\/\//, ""), "base64") - const type = await fileTypeFromBuffer(buffer) - return `${Date.now()}.${type.ext}` - } else { - return path.basename(file) - } - } catch (err) { - logger.error(`文件类型检测错误:${logger.red(err)}`) - } - return false + ws.sendMsg({ action, params, echo }) + return new Promise(resolve => Bot.once(echo, data => + resolve({ ...data, ...data.data }) + )) } - async uploadFile(data, file, name) { - const opts = { name: name || await this.fileName(file) || randomUUID() } + async uploadFile(data, file) { + file = await Bot.fileType(file, { http: true }) + const opts = { name: file.name } - if (file.match(/^https?:\/\//)) { - opts.type = "url" - opts.url = file - } else if (file.match(/^base64:\/\//)) { + if (Buffer.isBuffer(file.buffer)) { opts.type = "data" - opts.data = file.replace(/^base64:\/\//, "") - } else if (fs.existsSync(file)) { - opts.type = "data" - opts.data = fs.readFileSync(file).toString("base64") + opts.data = file.buffer.toString("base64") + } else if (file.buffer.match(/^https?:\/\//)) { + opts.type = "url" + opts.url = file.buffer } else { opts.type = "path" - opts.path = file + opts.path = file.buffer } - logger.info(`${logger.blue(`[${data.self_id}]`)} 上传文件:${this.makeLog(opts)}`) + Bot.makeLog("info", `上传文件:${this.makeLog(opts)}`, data.self_id) return data.bot.sendApi("upload_file", opts) } - async makeMsg(data, msg) { + async makeMsg(data, msg, send) { if (!Array.isArray(msg)) msg = [msg] const msgs = [] @@ -84,7 +49,7 @@ Bot.adapter.push(new class ComWeChatAdapter { else if (!i.data) i = { type: i.type, data: { ...i, type: undefined }} if (i.data.file) - i.data = { file_id: (await this.uploadFile(data, i.data.file, i.data.name)).file_id } + i.data = { file_id: (await this.uploadFile(data, i.data)).file_id } switch (i.type) { case "text": @@ -104,6 +69,10 @@ Bot.adapter.push(new class ComWeChatAdapter { i = { type: "mention", data: { user_id: i.data.qq }} break case "reply": + case "button": + continue + case "node": + await Bot.sendForwardMsg(send, i.data) continue default: i = { type: "text", data: { text: JSON.stringify(i) }} @@ -114,11 +83,8 @@ Bot.adapter.push(new class ComWeChatAdapter { } async sendFriendMsg(data, msg) { - if (msg?.type == "node") - return Bot.sendForwardMsg(msg => this.sendFriendMsg(data, msg), msg.data) - - const message = await this.makeMsg(data, msg) - logger.info(`${logger.blue(`[${data.self_id} => ${data.user_id}]`)} 发送好友消息:${this.makeLog(message)}`) + const message = await this.makeMsg(data, msg, msg => this.sendFriendMsg(data, msg)) + Bot.makeLog("info", `发送好友消息:${this.makeLog(message)}`, `${data.self_id} => ${data.user_id}`) return data.bot.sendApi("send_message", { detail_type: "private", user_id: data.user_id, @@ -127,11 +93,8 @@ Bot.adapter.push(new class ComWeChatAdapter { } async sendGroupMsg(data, msg) { - if (msg?.type == "node") - return Bot.sendForwardMsg(msg => this.sendGroupMsg(data, msg), msg.data) - - const message = await this.makeMsg(data, msg) - logger.info(`${logger.blue(`[${data.self_id} => ${data.group_id}]`)} 发送群消息:${this.makeLog(message)}`) + const message = await this.makeMsg(data, msg, msg => this.sendGroupMsg(data, msg)) + Bot.makeLog("info", `发送群消息:${this.makeLog(message)}`, `${data.self_id} => ${data.group_id}`) return data.bot.sendApi("send_message", { detail_type: "group", group_id: data.group_id, @@ -312,7 +275,7 @@ Bot.adapter.push(new class ComWeChatAdapter { data.bot.getFriendMap() data.bot.getGroupMap() - logger.mark(`${logger.blue(`[${data.self_id}]`)} ${this.name}(${this.id}) ${data.bot.version.impl}-${data.bot.version.version} 已连接`) + Bot.makeLog("mark", `${this.name}(${this.id}) ${data.bot.version.impl}-${data.bot.version.version} 已连接`, data.self_id) Bot.em(`connect.${data.self_id}`, data) } @@ -348,13 +311,13 @@ Bot.adapter.push(new class ComWeChatAdapter { switch (data.message_type) { case "private": - logger.info(`${logger.blue(`[${data.self_id}]`)} 好友消息:[${data.user_id}] ${data.raw_message}`) + Bot.makeLog("info", `好友消息:${data.raw_message}`, `${data.self_id} <= ${data.user_id}`) break case "group": - logger.info(`${logger.blue(`[${data.self_id}]`)} 群消息:[${data.group_id}, ${data.user_id}] ${data.raw_message}`) + Bot.makeLog("info", `群消息:${data.raw_message}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) break default: - logger.warn(`${logger.blue(`[${data.self_id}]`)} 未知消息:${logger.magenta(JSON.stringify(data))}`) + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) } Bot.em(`${data.post_type}.${data.message_type}`, data) @@ -369,43 +332,43 @@ Bot.adapter.push(new class ComWeChatAdapter { switch (data.detail_type) { case "private_message_delete": - logger.info(`${logger.blue(`[${data.self_id}]`)} 好友消息撤回:[${data.user_id}] ${data.message_id}`) + Bot.makeLog("info", `好友消息撤回:${data.message_id}`, `${data.self_id} <= ${data.user_id}`) data.sub_type = "recall" break case "group_message_delete": - logger.info(`${logger.blue(`[${data.self_id}]`)} 群消息撤回:[${data.group_id}, ${data.operator_id}=>${data.user_id}] ${data.message_id}`) + Bot.makeLog("info", `群消息撤回:${data.operator_id} => ${data.user_id} ${data.message_id}`, `${data.self_id} <= ${data.group_id}`) data.sub_type = "recall" break case "wx.get_private_file": - logger.info(`${logger.blue(`[${data.self_id}]`)} 私聊文件:[${data.user_id}] ${data.file_name} ${data.file_length} ${data.md5}`) + Bot.makeLog("info", `私聊文件:${data.file_name} ${data.file_length} ${data.md5}`, `${data.self_id} <= ${data.user_id}`) break case "wx.get_group_file": - logger.info(`${logger.blue(`[${data.self_id}]`)} 群文件:[${data.group_id}, ${data.user_id}] ${data.file_name} ${data.file_length} ${data.md5}`) + Bot.makeLog("info", `群文件:${data.file_name} ${data.file_length} ${data.md5}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) break case "wx.get_private_redbag": - logger.info(`${logger.blue(`[${data.self_id}]`)} 好友红包:[${data.user_id}]`) + Bot.makeLog("info", `好友红包`, `${data.self_id} <= ${data.user_id}`) break case "wx.get_group_redbag": - logger.info(`${logger.blue(`[${data.self_id}]`)} 群红包:[${data.group_id}, ${data.user_id}]`) + Bot.makeLog("info", `群红包`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) break case "wx.get_private_poke": data.operator_id = data.from_user_id data.target_id = data.user_id - logger.info(`${logger.blue(`[${data.self_id}]`)} 好友拍一拍:[${data.operator_id}=>${data.target_id}]`) + Bot.makeLog("info", `好友拍一拍:${data.operator_id} => ${data.target_id}`, data.self_id) break case "wx.get_group_poke": data.operator_id = data.from_user_id data.target_id = data.user_id - logger.info(`${logger.blue(`[${data.self_id}]`)} 群拍一拍:[${data.group_id}, ${data.operator_id}=>${data.target_id}]`) + Bot.makeLog("info", `群拍一拍:${data.operator_id} => ${data.target_id}`, `${data.self_id} <= ${data.group_id}`) break case "wx.get_private_card": - logger.info(`${logger.blue(`[${data.self_id}]`)} 好友用户名片:[${data.user_id}] ${data.v3} ${data.v4} ${data.nickname} ${data.head_url} ${data.province} ${data.city} ${data.sex}`) + Bot.makeLog("info", `好友用户名片:${data.v3} ${data.v4} ${data.nickname} ${data.head_url} ${data.province} ${data.city} ${data.sex}`, `${data.self_id} <= ${data.user_id}`) break case "wx.get_group_card": - logger.info(`${logger.blue(`[${data.self_id}]`)} 群用户名片:[${data.group_id}, ${data.user_id}] ${data.v3} ${data.v4} ${data.nickname} ${data.head_url} ${data.province} ${data.city} ${data.sex}`) + Bot.makeLog("info", `群用户名片:${data.v3} ${data.v4} ${data.nickname} ${data.head_url} ${data.province} ${data.city} ${data.sex}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) break default: - logger.warn(`${logger.blue(`[${data.self_id}]`)} 未知通知:${logger.magenta(JSON.stringify(data))}`) + Bot.makeLog("warn", `未知通知:${logger.magenta(data.raw)}`, data.self_id) } if (!data.sub_type) data.sub_type = data.detail_type.split("_").pop() @@ -422,11 +385,11 @@ Bot.adapter.push(new class ComWeChatAdapter { switch (data.detail_type) { case "wx.friend_request": - logger.info(`${logger.blue(`[${data.self_id}]`)} 加好友请求:[${data.user_id}] ${data.v3} ${data.v4} ${data.nickname} ${data.content} ${data.province} ${data.city}`) + Bot.makeLog("info", `加好友请求:${data.v3} ${data.v4} ${data.nickname} ${data.content} ${data.province} ${data.city}`, `${data.self_id} <= ${data.user_id}`) data.sub_type = "add" break default: - logger.warn(`${logger.blue(`[${data.self_id}]`)} 未知请求:${logger.magenta(JSON.stringify(data))}`) + Bot.makeLog("warn", `未知请求:${logger.magenta(data.raw)}`, data.self_id) } if (!data.sub_type) data.sub_type = data.detail_type.split("_").pop() @@ -444,15 +407,18 @@ Bot.adapter.push(new class ComWeChatAdapter { this.connect(data, ws) break default: - logger.warn(`${logger.blue(`[${data.self_id}]`)} 未知消息:${logger.magenta(JSON.stringify(data))}`) + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) } } message(data, ws) { try { - data = JSON.parse(data) + data = { + ...JSON.parse(data), + raw: Bot.String(data), + } } catch (err) { - return logger.error(`解码数据失败:${logger.red(err)}`) + return Bot.makeLog("error", ["解码数据失败", data, err]) } if (data.self?.user_id) { @@ -463,7 +429,7 @@ Bot.adapter.push(new class ComWeChatAdapter { if (data.type) { if (data.type != "meta" && !Bot.uin.includes(data.self_id)) { - logger.warn(`${logger.blue(`[${data.self_id}]`)} 找不到对应Bot,忽略消息:${logger.magenta(JSON.stringify(data))}`) + Bot.makeLog("warn", `找不到对应Bot,忽略消息:${logger.magenta(data.raw)}`, data.self_id) return false } data.bot = Bot[data.self_id] @@ -482,12 +448,12 @@ Bot.adapter.push(new class ComWeChatAdapter { this.makeRequest(data) break default: - logger.warn(`${logger.blue(`[${data.self_id}]`)} 未知消息:${logger.magenta(JSON.stringify(data))}`) + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) } } else if (data.echo) { Bot.emit(data.echo, data) } else { - logger.warn(`${logger.blue(`[${data.self_id}]`)} 未知消息:${logger.magenta(JSON.stringify(data))}`) + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) } } diff --git a/Yunzai/plugins/adapter/GSUIDCore.js b/Yunzai/plugins/adapter/GSUIDCore.js index c790d7f5a138c784d8d164d2e9ad03afa2024798..3c1f0ec26e3c5fbecb506336f154536a603fc5fe 100644 --- a/Yunzai/plugins/adapter/GSUIDCore.js +++ b/Yunzai/plugins/adapter/GSUIDCore.js @@ -1,7 +1,3 @@ -import { randomUUID } from "crypto" -import path from "node:path" -import fs from "node:fs" - Bot.adapter.push(new class GSUIDCoreAdapter { constructor() { this.id = "GSUIDCore" @@ -9,26 +5,55 @@ Bot.adapter.push(new class GSUIDCoreAdapter { this.path = this.id } - toStr(data) { - switch (typeof data) { - case "string": - return data - case "number": - return String(data) - case "object": - if (Buffer.isBuffer(data)) - return Buffer.from(data, "utf8").toString() - else - return JSON.stringify(data) - } - return data + makeLog(msg) { + return Bot.String(msg).replace(/base64:\/\/.*?"/g, "base64://...\"") } - makeLog(msg) { - return this.toStr(msg).replace(/base64:\/\/.*?"/g, "base64://...\"") + makeButton(data, button) { + const msg = { + text: button.text, + pressed_text: button.clicked_text, + ...button.GSUIDCore, + } + + if (button.input) { + msg.data = button.input + msg.action = 2 + } else if (button.callback) { + msg.data = button.callback + msg.action = 1 + } else if (button.link) { + msg.data = button.link + msg.action = 0 + } else return false + + if (button.permission) { + if (button.permission == "admin") { + msg.permission = 1 + } else { + msg.permission = 0 + if (!Array.isArray(button.permission)) + button.permission = [button.permission] + msg.specify_user_ids = button.permission + } + } + return msg + } + + makeButtons(button_square) { + const msgs = [] + for (const button_row of button_square) { + const buttons = [] + for (let button of button_row) { + button = this.makeButton(button) + if (button) buttons.push(button) + } + msgs.push(buttons) + } + return msgs } - makeMsg(msg) { + async makeMsg(msg) { if (!Array.isArray(msg)) msg = [msg] const msgs = [] @@ -36,6 +61,12 @@ Bot.adapter.push(new class GSUIDCoreAdapter { if (typeof i != "object") i = { type: "text", text: i } + if (i.file) { + i.file = await Bot.Buffer(i.file, { http: true }) + if (Buffer.isBuffer(i.file)) + i.file = `base64://${i.file.toString("base64")}` + } + switch (i.type) { case "text": i = { type: "text", data: i.text } @@ -44,7 +75,7 @@ Bot.adapter.push(new class GSUIDCoreAdapter { i = { type: "image", data: i.file } break case "record": - i = { type: "file", data: i.file } + i = { type: "record", data: i.file } break case "video": i = { type: "file", data: i.file } @@ -58,10 +89,15 @@ Bot.adapter.push(new class GSUIDCoreAdapter { case "reply": i = { type: "reply", data: i.id } break + case "button": + i = { type: "buttons", data: this.makeButtons(i.data) } + break + case "markdown": + break case "node": { const array = [] for (const { message } of i.data) - array.push(...this.makeMsg(message)) + array.push(...await this.makeMsg(message)) i.data = array break } default: @@ -72,9 +108,9 @@ Bot.adapter.push(new class GSUIDCoreAdapter { return msgs } - sendFriendMsg(data, msg) { - const content = this.makeMsg(msg) - logger.info(`${logger.blue(`[${data.self_id} => ${data.user_id}]`)} 发送好友消息:${this.makeLog(content)}`) + async sendFriendMsg(data, msg) { + const content = await this.makeMsg(msg) + Bot.makeLog("info", `发送好友消息:${this.makeLog(content)}`, `${data.self_id} => ${data.user_id}`) data.bot.sendApi({ bot_id: data.bot.bot_id, bot_self_id: data.bot.bot_self_id, @@ -85,10 +121,10 @@ Bot.adapter.push(new class GSUIDCoreAdapter { return { message_id: Date.now() } } - sendGroupMsg(data, msg) { + async sendGroupMsg(data, msg) { const target = data.group_id.split("-") - const content = this.makeMsg(msg) - logger.info(`${logger.blue(`[${data.self_id} => ${data.group_id}]`)} 发送群消息:${this.makeLog(content)}`) + const content = await this.makeMsg(msg) + Bot.makeLog("info", `发送群消息:${this.makeLog(content)}`, `${data.self_id} => ${data.group_id}`) data.bot.sendApi({ bot_id: data.bot.bot_id, bot_self_id: data.bot.bot_self_id, @@ -115,6 +151,7 @@ Bot.adapter.push(new class GSUIDCoreAdapter { pickMember(id, group_id, user_id) { const i = { ...Bot[id].fl.get(user_id), + ...Bot[id].gml.get(group_id)?.get(user_id), self_id: id, bot: Bot[id], group_id: group_id, @@ -146,8 +183,8 @@ Bot.adapter.push(new class GSUIDCoreAdapter { ws: ws, get sendApi() { return this.ws.sendMsg }, uin: data.self_id, - bot_id: data.bot_id, - bot_self_id: data.bot_self_id, + bot_id: data.raw.bot_id, + bot_self_id: data.raw.bot_self_id, stat: { start_time: Date.now()/1000 }, version: { id: this.id, @@ -161,19 +198,34 @@ Bot.adapter.push(new class GSUIDCoreAdapter { gl: new Map, gml: new Map, } + data.bot = Bot[data.self_id] - logger.mark(`${logger.blue(`[${data.self_id}]`)} ${this.name}(${this.id}) 已连接`) + Bot.makeLog("mark", `${this.name}(${this.id}) 已连接`, data.self_id) Bot.em(`connect.${data.self_id}`, data) } - message(data, ws) { + message(raw, ws) { try { - data = JSON.parse(data) + raw = JSON.parse(raw) } catch (err) { - return logger.error(`解码数据失败:${logger.red(err)}`) + return Bot.makeLog("error", ["解码数据失败", raw, err]) + } + + const data = { + raw, + self_id: raw.bot_self_id, + post_type: "message", + message_id: raw.msg_id, + get user_id() { return this.sender.user_id }, + sender: { + ...raw.sender, + user_id: raw.user_id, + user_pm: raw.user_pm, + }, + message: [], + raw_message: "", } - data.self_id = data.bot_self_id if (Bot[data.self_id]) { data.bot = Bot[data.self_id] data.bot.ws = ws @@ -181,19 +233,10 @@ Bot.adapter.push(new class GSUIDCoreAdapter { this.makeBot(data, ws) } - data.post_type = "message" - data.message_id = data.msg_id - data.user_id = data.user_id - data.sender = { - user_id: data.user_id, - user_pm: data.user_pm, - } if (!data.bot.fl.has(data.user_id)) data.bot.fl.set(data.user_id, data.sender) - data.message = [] - data.raw_message = "" - for (const i of data.content) { + for (const i of raw.content) { switch (i.type) { case "text": data.message.push({ type: "text", text: i.data }) @@ -225,15 +268,24 @@ Bot.adapter.push(new class GSUIDCoreAdapter { } } - if (data.user_type == "direct") { + if (raw.user_type == "direct") { data.message_type = "private" - logger.info(`${logger.blue(`[${data.self_id}]`)} 好友消息:[${data.user_id}] ${data.raw_message}`) + Bot.makeLog("info", `好友消息:${data.raw_message}`, `${data.self_id} <= ${data.user_id}`) } else { data.message_type = "group" - data.group_id = `${data.user_type}-${data.group_id}` + data.group_id = `${raw.user_type}-${raw.group_id}` + if (!data.bot.gl.has(data.group_id)) data.bot.gl.set(data.group_id, { group_id: data.group_id }) - logger.info(`${logger.blue(`[${data.self_id}]`)} 群消息:[${data.group_id}, ${data.user_id}] ${data.raw_message}`) + let gml = data.bot.gml.get(data.group_id) + if (!gml) { + gml = new Map + data.bot.gml.set(data.group_id, gml) + } + if (!gml.has(data.user_id)) + gml.set(data.user_id, data.sender) + + Bot.makeLog("info", `群消息:${data.raw_message}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) } Bot.em(`${data.post_type}.${data.message_type}`, data) diff --git a/Yunzai/plugins/adapter/OPQBot.js b/Yunzai/plugins/adapter/OPQBot.js new file mode 100644 index 0000000000000000000000000000000000000000..ba4bd4a772ced364bdec3073f7ba93e057ad1541 --- /dev/null +++ b/Yunzai/plugins/adapter/OPQBot.js @@ -0,0 +1,327 @@ +Bot.adapter.push(new class OPQBotAdapter { + constructor() { + this.id = "QQ" + this.name = "OPQBot" + this.path = this.name + this.CommandId = { + FriendImage: 1, + GroupImage: 2, + FriendVoice: 26, + GroupVoice: 29, + } + } + + sendApi(id, CgiCmd, CgiRequest) { + const ReqId = Math.round(Math.random()*10**16) + Bot[id].ws.sendMsg({ BotUin: String(id), CgiCmd, CgiRequest, ReqId }) + return new Promise(resolve => + Bot.once(ReqId, data => resolve(data))) + } + + makeLog(msg) { + return Bot.String(msg).replace(/base64:\/\/.*?"/g, 'base64://..."') + } + + async uploadFile(id, type, file) { + const opts = { CommandId: this.CommandId[type] } + + file = await Bot.Buffer(file, { http: true }) + if (Buffer.isBuffer(file)) + opts.Base64Buf = file.toString("base64") + else if (file.match(/^https?:\/\//)) + opts.FileUrl = file + else + opts.FilePath = file + + return (await this.sendApi(id, "PicUp.DataUp", opts)).ResponseData + } + + async sendMsg(send, upload, msg) { + if (!Array.isArray(msg)) + msg = [msg] + const message = { + Content: "", + Images: [], + AtUinLists: [], + } + + for (let i of msg) { + if (typeof i != "object") + i = { type: "text", text: i } + + switch (i.type) { + case "text": + message.Content += i.text + break + case "image": + message.Images.push(await upload("Image", i.file)) + break + case "record": + message.Voice = await upload("Voice", i.file) + break + case "at": + message.AtUinLists.push({ Uin: i.qq }) + break + case "video": + case "file": + case "face": + case "reply": + case "button": + continue + case "node": + await Bot.sendForwardMsg(msg => this.sendMsg(send, upload, msg), i.data) + continue + default: + message.Content += JSON.stringify(i) + } + } + + return send(message) + } + + sendFriendMsg(data, msg, event) { + Bot.makeLog("info", `发送好友消息:${this.makeLog(msg)}`, `${data.self_id} => ${data.user_id}`) + return this.sendMsg( + msg => this.sendApi(data.self_id, + "MessageSvc.PbSendMsg", { + ToUin: data.user_id, + ToType: 1, + ...msg, + }), + (type, file) => this.uploadFile(data.self_id, `Friend${type}`, file), + msg + ) + } + + sendMemberMsg(data, msg, event) { + Bot.makeLog("info", `发送群员消息:${this.makeLog(msg)}`, `${data.self_id} => ${data.group_id}, ${data.user_id}`) + return this.sendMsg( + msg => this.sendApi(data.self_id, + "MessageSvc.PbSendMsg", { + ToUin: data.user_id, + GroupCode: data.group_id, + ToType: 3, + ...msg, + }), + (type, file) => this.uploadFile(data.self_id, `Friend${type}`, file), + msg + ) + } + + sendGroupMsg(data, msg) { + Bot.makeLog("info", `发送群消息:${this.makeLog(msg)}`, `${data.self_id} => ${data.group_id}`) + let ReplyTo + if (data.message_id && data.seq && data.time) + ReplyTo = { + MsgSeq: data.seq, + MsgTime: data.time, + MsgUid: data.message_id, + } + + return this.sendMsg( + msg => this.sendApi(data.self_id, + "MessageSvc.PbSendMsg", { + ToUin: data.group_id, + ToType: 2, + ReplyTo, + ...msg, + }), + (type, file) => this.uploadFile(data.self_id, `Group${type}`, file), + msg + ) + } + + pickFriend(id, user_id) { + const i = { + ...Bot[id].fl.get(user_id), + self_id: id, + bot: Bot[id], + user_id: user_id, + } + return { + ...i, + sendMsg: msg => this.sendFriendMsg(i, msg), + getAvatarUrl: () => `https://q1.qlogo.cn/g?b=qq&s=0&nk=${user_id}`, + } + } + + pickMember(id, group_id, user_id) { + const i = { + ...Bot[id].fl.get(user_id), + self_id: id, + bot: Bot[id], + user_id: user_id, + group_id: group_id, + } + return { + ...this.pickFriend(id, user_id), + ...i, + sendMsg: msg => this.sendMemberMsg(i, msg), + } + } + + pickGroup(id, group_id) { + const i = { + ...Bot[id].gl.get(group_id), + self_id: id, + bot: Bot[id], + group_id: group_id, + } + return { + ...i, + sendMsg: msg => this.sendGroupMsg(i, msg), + pickMember: user_id => this.pickMember(id, group_id, user_id), + getAvatarUrl: () => `https://p.qlogo.cn/gh/${group_id}/${group_id}/0`, + } + } + + makeMessage(id, event) { + const data = { + event, + bot: Bot[id], + self_id: id, + post_type: "message", + message_id: event.MsgHead.MsgUid, + seq: event.MsgHead.MsgSeq, + time: event.MsgHead.MsgTime, + user_id: event.MsgHead.SenderUin, + sender: { + user_id: event.MsgHead.SenderUin, + nickname: event.MsgHead.SenderNick, + }, + message: [], + raw_message: "", + } + + if (event.MsgBody.AtUinLists) + for (const i of event.MsgBody.AtUinLists) { + data.message.push({ + type: "at", + qq: i.Uin, + data: i, + }) + data.raw_message += `[提及:${i.Uin}]` + } + + if (event.MsgBody.Content) { + data.message.push({ + type: "text", + text: event.MsgBody.Content, + }) + data.raw_message += event.MsgBody.Content + } + + if (event.MsgBody.Images) + for (const i of event.MsgBody.Images) { + data.message.push({ + type: "image", + url: i.Url, + data: i, + }) + data.raw_message += `[图片:${i.Url}]` + } + + return data + } + + makeFriendMessage(id, data) { + if (!data.MsgBody) return + data = this.makeMessage(id, data) + data.message_type = "private" + + Bot.makeLog("info", `好友消息:[${data.sender.nickname}] ${data.raw_message}`, `${data.self_id} <= ${data.user_id}`) + Bot.em(`${data.post_type}.${data.message_type}`, data) + } + + makeGroupMessage(id, data) { + if (!data.MsgBody) return + data = this.makeMessage(id, data) + data.message_type = "group" + data.sender.card = data.event.MsgHead.GroupInfo.GroupCard + data.group_id = data.event.MsgHead.GroupInfo.GroupCode + data.group_name = data.event.MsgHead.GroupInfo.GroupName + + data.reply = msg => this.sendGroupMsg(data, msg) + Bot.makeLog("info", `群消息:[${data.group_name}, ${data.sender.nickname}] ${data.raw_message}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + Bot.em(`${data.post_type}.${data.message_type}`, data) + } + + makeEvent(id, data) { + switch (data.EventName) { + case "ON_EVENT_FRIEND_NEW_MSG": + this.makeFriendMessage(id, data.EventData) + break + case "ON_EVENT_GROUP_NEW_MSG": + this.makeGroupMessage(id, data.EventData) + break + default: + Bot.makeLog("warn", `未知事件:${logger.magenta(data.raw)}`, id) + } + } + + makeBot(id, ws) { + Bot[id] = { + adapter: this, + ws, + + uin: id, + info: { id }, + get nickname() { return this.info.nickname }, + get avatar() { return `https://q1.qlogo.cn/g?b=qq&s=0&nk=${this.uin}` }, + + version: { + id: this.id, + name: this.name, + version: this.version, + }, + stat: { start_time: Date.now()/1000 }, + + pickFriend: user_id => this.pickFriend(id, user_id), + get pickUser() { return this.pickFriend }, + getFriendMap() { return this.fl }, + fl: new Map, + + pickMember: (group_id, user_id) => this.pickMember(id, group_id, user_id), + pickGroup: group_id => this.pickGroup(id, group_id), + getGroupMap() { return this.gl }, + gl: new Map, + gml: new Map, + } + + Bot.makeLog("mark", `${this.name}(${this.id}) ${this.version} 已连接`, id) + Bot.em(`connect.${id}`, { self_id: id }) + } + + message(data, ws) { + try { + data = { + ...JSON.parse(data), + raw: Bot.String(data), + } + } catch (err) { + return Bot.makeLog("error", ["解码数据失败", data, err]) + } + + const id = data.CurrentQQ + if (id && data.CurrentPacket) { + if (Bot[id]) + Bot[id].ws = ws + else + this.makeBot(id, ws) + + this.makeEvent(id, data.CurrentPacket) + } else if (data.ReqId) { + Bot.emit(data.ReqId, data) + } else { + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, id) + } + } + + load() { + if (!Array.isArray(Bot.wsf[this.path])) + Bot.wsf[this.path] = [] + Bot.wsf[this.path].push((ws, ...args) => + ws.on("message", data => this.message(data, ws, ...args)) + ) + } +}) \ No newline at end of file diff --git a/Yunzai/plugins/adapter/OneBotv11.js b/Yunzai/plugins/adapter/OneBotv11.js new file mode 100644 index 0000000000000000000000000000000000000000..bb861fc43567520d2f969facb08665d5c159e14f --- /dev/null +++ b/Yunzai/plugins/adapter/OneBotv11.js @@ -0,0 +1,989 @@ +import { randomUUID } from "node:crypto" +import path from "node:path" + +Bot.adapter.push(new class OneBotv11Adapter { + constructor() { + this.id = "QQ" + this.name = "OneBotv11" + this.path = this.name + } + + makeLog(msg) { + return Bot.String(msg).replace(/base64:\/\/.*?(,|]|")/g, "base64://...$1") + } + + sendApi(ws, action, params) { + const echo = randomUUID() + ws.sendMsg({ action, params, echo }) + return new Promise(resolve => Bot.once(echo, data => + resolve({ ...data, ...data.data }) + )) + } + + setProfile(data, profile) { + Bot.makeLog("info", `设置资料:${JSON.stringify(profile)}`, data.self_id) + return data.bot.sendApi("set_qq_profile", profile) + } + + async makeFile(file) { + file = await Bot.Buffer(file, { http: true }) + if (Buffer.isBuffer(file)) + file = `base64://${file.toString("base64")}` + return file + } + + async makeMsg(msg) { + if (!Array.isArray(msg)) + msg = [msg] + const msgs = [] + const forward = [] + for (let i of msg) { + if (typeof i != "object") + i = { type: "text", data: { text: i }} + else if (!i.data) + i = { type: i.type, data: { ...i, type: undefined }} + + switch (i.type) { + case "at": + i.data.qq = String(i.data.qq) + break + case "reply": + i.data.id = String(i.data.id) + break + case "button": + continue + case "node": + forward.push(...i.data) + continue + } + + if (i.data.file) + i.data.file = await this.makeFile(i.data.file) + + msgs.push(i) + } + return [msgs, forward] + } + + async sendMsg(msg, send, sendForwardMsg) { + const [message, forward] = await this.makeMsg(msg) + const ret = [] + + if (forward.length) { + const data = await sendForwardMsg(forward) + if (Array.isArray(data)) + ret.push(...data) + else + ret.push(data) + } + + if (message.length) + ret.push(await send(message)) + if (ret.length == 1) return ret[0] + + const message_id = [] + for (const i of ret) if (i?.message_id) + message_id.push(i.message_id) + return { data: ret, message_id } + } + + sendFriendMsg(data, msg) { + return this.sendMsg(msg, message => { + Bot.makeLog("info", `发送好友消息:${this.makeLog(message)}`, `${data.self_id} => ${data.user_id}`) + data.bot.sendApi("send_msg", { + user_id: data.user_id, + message, + }) + }, msg => this.sendFriendForwardMsg(data, msg)) + } + + sendGroupMsg(data, msg) { + return this.sendMsg(msg, message => { + Bot.makeLog("info", `发送群消息:${this.makeLog(message)}`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("send_msg", { + group_id: data.group_id, + message, + }) + }, msg => this.sendGroupForwardMsg(data, msg)) + } + + sendGuildMsg(data, msg) { + return this.sendMsg(msg, message => { + Bot.makeLog("info", `发送频道消息:${this.makeLog(message)}`, `${data.self_id}] => ${data.guild_id}-${data.channel_id}`) + return data.bot.sendApi("send_guild_channel_msg", { + guild_id: data.guild_id, + channel_id: data.channel_id, + message, + }) + }, msg => Bot.sendForwardMsg(msg => this.sendGuildMsg(data, msg), msg)) + } + + async recallMsg(data, message_id) { + Bot.makeLog("info", `撤回消息:${message_id}`, data.self_id) + if (!Array.isArray(message_id)) + message_id = [message_id] + const msgs = [] + for (const i of message_id) + msgs.push(await data.bot.sendApi("delete_msg", { message_id: i })) + return msgs + } + + parseMsg(msg) { + const array = [] + for (const i of Array.isArray(msg) ? msg : [msg]) + if (typeof i == "object") + array.push({ ...i.data, type: i.type }) + else + array.push({ type: "text", text: String(i) }) + return array + } + + async getMsg(data, message_id) { + const msg = (await data.bot.sendApi("get_msg", { message_id })).data + if (msg?.message) + msg.message = this.parseMsg(msg.message) + return msg + } + + async getGroupMsgHistory(data, message_seq, count) { + const msgs = (await data.bot.sendApi("get_group_msg_history", { + group_id: data.group_id, + message_seq, + count, + })).data?.messages + + for (const i of Array.isArray(msgs) ? msgs : [msgs]) + if (i?.message) + i.message = this.parseMsg(i.message) + return msgs + } + + async getForwardMsg(data, message_id) { + const msgs = (await data.bot.sendApi("get_forward_msg", { + message_id, + })).data?.messages + + for (const i of Array.isArray(msgs) ? msgs : [msgs]) + if (i?.message) + i.message = this.parseMsg(i.message || i.content) + return msgs + } + + async makeForwardMsg(msg) { + const msgs = [] + for (const i of msg) { + const [content, forward] = await this.makeMsg(i.message) + if (forward.length) + msgs.push(...await this.makeForwardMsg(forward)) + if (content.length) + msgs.push({ type: "node", data: { + name: i.nickname || "匿名消息", + uin: String(Number(i.user_id) || 80000000), + content, + time: i.time, + }}) + } + return msgs + } + + async sendFriendForwardMsg(data, msg) { + Bot.makeLog("info", `发送好友转发消息:${this.makeLog(msg)}`, `${data.self_id} => ${data.user_id}`) + return data.bot.sendApi("send_private_forward_msg", { + user_id: data.user_id, + messages: await this.makeForwardMsg(msg), + }) + } + + async sendGroupForwardMsg(data, msg) { + Bot.makeLog("info", `发送群转发消息:${this.makeLog(msg)}`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("send_group_forward_msg", { + group_id: data.group_id, + messages: await this.makeForwardMsg(msg), + }) + } + + async getFriendArray(data) { + return (await data.bot.sendApi("get_friend_list")).data || [] + } + + async getFriendList(data) { + const array = [] + for (const { user_id } of await this.getFriendArray(data)) + array.push(user_id) + return array + } + + async getFriendMap(data) { + const map = new Map + for (const i of await this.getFriendArray(data)) + map.set(i.user_id, i) + data.bot.fl = map + return map + } + + getFriendInfo(data) { + return data.bot.sendApi("get_stranger_info", { + user_id: data.user_id, + }) + } + + async getGroupArray(data) { + const array = (await data.bot.sendApi("get_group_list")).data + try { for (const guild of await this.getGuildArray(data)) + for (const channel of await this.getGuildChannelArray({ + ...data, + guild_id: guild.guild_id, + })) + array.push({ + guild, + channel, + group_id: `${guild.guild_id}-${channel.channel_id}`, + group_name: `${guild.guild_name}-${channel.channel_name}`, + }) + } catch (err) { + Bot.makeLog("error", ["获取频道列表错误", err]) + } + return array + } + + async getGroupList(data) { + const array = [] + for (const { group_id } of await this.getGroupArray(data)) + array.push(group_id) + return array + } + + async getGroupMap(data) { + const map = new Map + for (const i of await this.getGroupArray(data)) + map.set(i.group_id, i) + data.bot.gl = map + return map + } + + getGroupInfo(data) { + return data.bot.sendApi("get_group_info", { + group_id: data.group_id, + }) + } + + async getMemberArray(data) { + return (await data.bot.sendApi("get_group_member_list", { + group_id: data.group_id, + })).data || [] + } + + async getMemberList(data) { + const array = [] + for (const { user_id } of await this.getMemberArray(data)) + array.push(user_id) + return array + } + + async getMemberMap(data) { + const map = new Map + for (const i of await this.getMemberArray(data)) + map.set(i.user_id, i) + data.bot.gml.set(data.group_id, map) + return map + } + + async getGroupMemberMap(data) { + for (const [group_id, group] of await this.getGroupMap(data)) { + if (group.guild) continue + await this.getMemberMap({ ...data, group_id }) + } + } + + getMemberInfo(data) { + return data.bot.sendApi("get_group_member_info", { + group_id: data.group_id, + user_id: data.user_id, + }) + } + + async getGuildArray(data) { + return (await data.bot.sendApi("get_guild_list")).data || [] + } + + getGuildInfo(data) { + return data.bot.sendApi("get_guild_meta_by_guest", { + guild_id: data.guild_id, + }) + } + + async getGuildChannelArray(data) { + return (await data.bot.sendApi("get_guild_channel_list", { + guild_id: data.guild_id, + })).data || [] + } + + async getGuildChannelMap(data) { + const map = new Map + for (const i of await this.getGuildChannelArray(data)) + map.set(i.channel_id, i) + return map + } + + async getGuildMemberArray(data) { + const array = [] + let next_token = "" + while (true) { + const list = (await data.bot.sendApi("get_guild_member_list", { + guild_id: data.guild_id, + next_token, + })).data + if (!list) break + + for (const i of list.members) + array.push({ + ...i, + user_id: i.tiny_id, + }) + if (list.finished) break + next_token = list.next_token + } + return array + } + + async getGuildMemberList(data) { + const array = [] + for (const { user_id } of await this.getGuildMemberArray(data)) + array.push(user_id) + return array.push + } + + async getGuildMemberMap(data) { + const map = new Map + for (const i of await this.getGuildMemberArray(data)) + map.set(i.user_id, i) + data.bot.gml.set(data.group_id, map) + return map + } + + getGuildMemberInfo(data) { + return data.bot.sendApi("get_guild_member_profile", { + guild_id: data.guild_id, + user_id: data.user_id, + }) + } + + sendLike(data, times) { + Bot.makeLog("info", `点赞:${times}次`, `${data.self_id} => ${data.user_id}`) + return data.bot.sendApi("send_like", { + user_id: data.user_id, + times, + }) + } + + setGroupName(data, group_name) { + Bot.makeLog("info", `设置群名:${group_name}`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("set_group_name", { + group_id: data.group_id, + group_name, + }) + } + + async setGroupAvatar(data, file) { + Bot.makeLog("info", `设置群头像:${file}`, `${data.self_id} => ${data.group_id}`) + file = await Bot.Buffer(file, { http: true }) + return data.bot.sendApi("set_group_portrait", { + group_id: data.group_id, + file, + }) + } + + setGroupAdmin(data, user_id, enable) { + Bot.makeLog("info", `${enable ? "设置" : "取消"}群管理员:${user_id}`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("set_group_admin", { + group_id: data.group_id, + user_id, + enable, + }) + } + + setGroupCard(data, user_id, card) { + Bot.makeLog("info", `设置群名片:${card}`, `${data.self_id} => ${data.group_id}, ${user_id}`) + return data.bot.sendApi("set_group_card", { + group_id: data.group_id, + user_id, + card, + }) + } + + setGroupTitle(data, user_id, special_title, duration) { + Bot.makeLog("info", `设置群头衔:${special_title} ${duration}`, `${data.self_id} => ${data.group_id}, ${user_id}`) + return data.bot.sendApi("set_group_special_title", { + group_id: data.group_id, + user_id, + special_title, + duration, + }) + } + + sendGroupSign(data) { + Bot.makeLog("info", "群打卡", `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("send_group_sign", { + group_id: data.group_id, + }) + } + + setGroupBan(data, user_id, duration) { + Bot.makeLog("info", `禁言群成员:${duration}秒`, `${data.self_id} => ${data.group_id}, ${user_id}`) + return data.bot.sendApi("set_group_ban", { + group_id: data.group_id, + user_id, + duration, + }) + } + + setGroupWholeKick(data, enable) { + Bot.makeLog("info", `${enable ? "开启" : "关闭"}全员禁言`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("set_group_whole_ban", { + group_id: data.group_id, + enable, + }) + } + + setGroupKick(data, user_id, reject_add_request) { + Bot.makeLog("info", `踢出群成员${reject_add_request ? "拒绝再次加群" : ""}`, `${data.self_id} => ${data.group_id}, ${user_id}`) + return data.bot.sendApi("set_group_kick", { + group_id: data.group_id, + user_id, + reject_add_request, + }) + } + + setGroupLeave(data, is_dismiss) { + Bot.makeLog("info", is_dismiss ? "解散" : "退群", `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("set_group_leave", { + group_id: data.group_id, + is_dismiss, + }) + } + + downloadFile(data, url, thread_count, headers) { + return data.bot.sendApi("download_file", { + url, + thread_count, + headers, + }) + } + + async sendFriendFile(data, file, name = path.basename(file)) { + Bot.makeLog("info", `发送好友文件:${name}(${file})`, `${data.self_id} => ${data.user_id}`) + return data.bot.sendApi("upload_private_file", { + user_id: data.user_id, + file: await this.makeFile(file), + name, + }) + } + + async sendGroupFile(data, file, folder, name = path.basename(file)) { + Bot.makeLog("info", `发送群文件:${folder||""}/${name}(${file})`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("upload_group_file", { + group_id: data.group_id, + folder, + file: await this.makeFile(file), + name, + }) + } + + deleteGroupFile(data, file_id, busid) { + Bot.makeLog("info", `删除群文件:${file_id}(${busid})`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("delete_group_file", { + group_id: data.group_id, + file_id, + busid, + }) + } + + createGroupFileFolder(data, name) { + Bot.makeLog("info", `创建群文件夹:${name}`, `${data.self_id} => ${data.group_id}`) + return data.bot.sendApi("create_group_file_folder", { + group_id: data.group_id, + name, + }) + } + + getGroupFileSystemInfo(data) { + return data.bot.sendApi("get_group_file_system_info", { + group_id: data.group_id, + }) + } + + getGroupFiles(data, folder_id) { + if (folder_id) + return data.bot.sendApi("get_group_files_by_folder", { + group_id: data.group_id, + folder_id, + }) + return data.bot.sendApi("get_group_root_files", { + group_id: data.group_id, + }) + } + + getGroupFileUrl(data, file_id, busid) { + return data.bot.sendApi("get_group_file_url", { + group_id: data.group_id, + file_id, + busid, + }) + } + + getGroupFs(data) { + return { + upload: (file, folder, name) => this.sendGroupFile(data, file, folder, name), + rm: (file_id, busid) => this.deleteGroupFile(data, file_id, busid), + mkdir: name => this.createGroupFileFolder(data, name), + df: () => this.getGroupFileSystemInfo(data), + ls: folder_id => this.getGroupFiles(data, folder_id), + download: (file_id, busid) => this.getGroupFileUrl(data, file_id, busid), + } + } + + setFriendAddRequest(data, flag, approve, remark) { + return data.bot.sendApi("set_friend_add_request", { + flag, + approve, + remark, + }) + } + + setGroupAddRequest(data, flag, sub_type, approve, reason) { + return data.bot.sendApi("set_group_add_request", { + flag, + sub_type, + approve, + reason, + }) + } + + pickFriend(data, user_id) { + const i = { + ...data.bot.fl.get(user_id), + ...data, + user_id, + } + return { + ...i, + sendMsg: msg => this.sendFriendMsg(i, msg), + getMsg: message_id => this.getMsg(i, message_id), + recallMsg: message_id => this.recallMsg(i, message_id), + getForwardMsg: message_id => this.getForwardMsg(i, message_id), + sendForwardMsg: msg => this.sendFriendForwardMsg(i, msg), + sendFile: (file, name) => this.sendFriendFile(i, file, name), + getInfo: () => this.getFriendInfo(i), + getAvatarUrl: () => `https://q1.qlogo.cn/g?b=qq&s=0&nk=${user_id}`, + thumbUp: times => this.sendLike(i, times), + } + } + + pickMember(data, group_id, user_id) { + if (typeof group_id == "string" && group_id.match("-")) { + const guild_id = group_id.split("-") + const i = { + ...data, + guild_id: guild_id[0], + channel_id: guild_id[1], + user_id, + } + return { + ...this.pickGroup(i, group_id), + ...i, + getInfo: () => this.getGuildMemberInfo(i), + getAvatarUrl: async () => (await this.getGuildMemberInfo(i)).avatar_url, + } + } + + const i = { + ...data.bot.fl.get(user_id), + ...data.bot.gml.get(group_id)?.get(user_id), + ...data, + group_id, + user_id, + } + return { + ...this.pickFriend(i, user_id), + ...i, + getInfo: () => this.getMemberInfo(i), + poke: () => this.sendGroupMsg(i, { type: "poke", qq: user_id }), + mute: duration => this.setGroupBan(i, i.user_id, duration), + kick: reject_add_request => this.setGroupKick(i, i.user_id, reject_add_request), + get is_friend() { return data.bot.fl.has(user_id) }, + get is_owner() { return i.role == "owner" }, + get is_admin() { return i.role == "admin" }, + } + } + + pickGroup(data, group_id) { + if (typeof group_id == "string" && group_id.match("-")) { + const guild_id = group_id.split("-") + const i = { + ...data.bot.gl.get(group_id), + ...data, + guild_id: guild_id[0], + channel_id: guild_id[1], + } + return { + ...i, + sendMsg: msg => this.sendGuildMsg(i, msg), + getMsg: message_id => this.getMsg(i, message_id), + recallMsg: message_id => this.recallMsg(i, message_id), + getForwardMsg: message_id => this.getForwardMsg(i, message_id), + getInfo: () => this.getGuildInfo(i), + getChannelArray: () => this.getGuildChannelArray(i), + getChannelList: () => this.getGuildChannelList(i), + getChannelMap: () => this.getGuildChannelMap(i), + getMemberArray: () => this.getGuildMemberArray(i), + getMemberList: () => this.getGuildMemberList(i), + getMemberMap: () => this.getGuildMemberMap(i), + pickMember: user_id => this.pickMember(i, group_id, user_id), + } + } + + const i = { + ...data.bot.gl.get(group_id), + ...data, + group_id, + } + return { + ...i, + sendMsg: msg => this.sendGroupMsg(i, msg), + getMsg: message_id => this.getMsg(i, message_id), + recallMsg: message_id => this.recallMsg(i, message_id), + getForwardMsg: message_id => this.getForwardMsg(i, message_id), + sendForwardMsg: msg => this.sendGroupForwardMsg(i, msg), + sendFile: (file, name) => this.sendGroupFile(i, file, undefined, name), + getInfo: () => this.getGroupInfo(i), + getAvatarUrl: () => `https://p.qlogo.cn/gh/${group_id}/${group_id}/0`, + getChatHistory: (seq, cnt) => this.getGroupMsgHistory(i, seq, cnt), + getMemberArray: () => this.getMemberArray(i), + getMemberList: () => this.getMemberList(i), + getMemberMap: () => this.getMemberMap(i), + pickMember: user_id => this.pickMember(i, group_id, user_id), + pokeMember: qq => this.sendGroupMsg(i, { type: "poke", qq }), + setName: group_name => this.setGroupName(i, group_name), + setAvatar: file => this.setGroupAvatar(i, file), + setAdmin: (user_id, enable) => this.setGroupAdmin(i, user_id, enable), + setCard: (user_id, card) => this.setGroupCard(i, user_id, card), + setTitle: (user_id, special_title, duration) => this.setGroupTitle(i, user_id, special_title, duration), + sign: () => this.sendGroupSign(i), + muteMember: (user_id, duration) => this.setGroupBan(i, user_id, duration), + muteAll: enable => this.setGroupWholeKick(i, enable), + kickMember: (user_id, reject_add_request) => this.setGroupKick(i, user_id, reject_add_request), + quit: is_dismiss => this.setGroupLeave(i, is_dismiss), + fs: this.getGroupFs(i), + get is_owner() { return data.bot.gml.get(group_id)?.get(data.self_id)?.role == "owner" }, + get is_admin() { return data.bot.gml.get(group_id)?.get(data.self_id)?.role == "admin" }, + } + } + + async connect(data, ws) { + Bot[data.self_id] = { + adapter: this, + ws: ws, + sendApi: (action, params) => this.sendApi(ws, action, params), + stat: { + start_time: data.time, + stat: {}, + get lost_pkt_cnt() { return this.stat.packet_lost }, + get lost_times() { return this.stat.lost_times }, + get recv_msg_cnt() { return this.stat.message_received }, + get recv_pkt_cnt() { return this.stat.packet_received }, + get sent_msg_cnt() { return this.stat.message_sent }, + get sent_pkt_cnt() { return this.stat.packet_sent }, + }, + model: "TRSS Yunzai ", + + info: {}, + get uin() { return this.info.user_id }, + get nickname() { return this.info.nickname }, + get avatar() { return `https://q1.qlogo.cn/g?b=qq&s=0&nk=${this.uin}` }, + + setProfile: profile => this.setProfile(data, profile), + setNickname: nickname => this.setProfile(data, { nickname }), + + pickFriend: user_id => this.pickFriend(data, user_id), + get pickUser() { return this.pickFriend }, + getFriendArray: () => this.getFriendArray(data), + getFriendList: () => this.getFriendList(data), + getFriendMap: () => this.getFriendMap(data), + fl: new Map, + + pickMember: (group_id, user_id) => this.pickMember(data, group_id, user_id), + pickGroup: group_id => this.pickGroup(data, group_id), + getGroupArray: () => this.getGroupArray(data), + getGroupList: () => this.getGroupList(data), + getGroupMap: () => this.getGroupMap(data), + getGroupMemberMap: () => this.getGroupMemberMap(data), + gl: new Map, + gml: new Map, + + request_list: [], + getSystemMsg: () => data.bot.request_list, + setFriendAddRequest: (flag, approve, remark) => this.setFriendAddRequest(data, flag, approve, remark), + setGroupAddRequest: (flag, sub_type, approve, reason) => this.setGroupAddRequest(data, flag, sub_type, approve, reason), + } + data.bot = Bot[data.self_id] + + if (!Bot.uin.includes(data.self_id)) + Bot.uin.push(data.self_id) + + data.bot.sendApi("_set_model_show", { + model: data.bot.model, + model_show: data.bot.model, + }) + + data.bot.info = (await data.bot.sendApi("get_login_info")).data + data.bot.guild_info = (await data.bot.sendApi("get_guild_service_profile")).data + data.bot.clients = (await data.bot.sendApi("get_online_clients")).clients + data.bot.version = { + ...(await data.bot.sendApi("get_version_info")).data, + id: this.id, + name: this.name, + get version() { + return this.app_full_name || `${this.app_name} v${this.app_version}` + }, + } + + data.bot.getFriendMap() + data.bot.getGroupMemberMap() + + Bot.makeLog("mark", `${this.name}(${this.id}) ${data.bot.version.version} 已连接`, data.self_id) + Bot.em(`connect.${data.self_id}`, data) + } + + makeMessage(data) { + data.message = this.parseMsg(data.message) + switch (data.message_type) { + case "private": { + const name = data.sender.card || data.sender.nickname || data.bot.fl.get(data.user_id)?.nickname + Bot.makeLog("info", `好友消息:${name ? `[${name}] ` : ""}${data.raw_message}`, `${data.self_id} <= ${data.user_id}`) + break + } case "group": { + const group_name = data.group_name || data.bot.gl.get(data.group_id)?.group_name + let user_name = data.sender.card || data.sender.nickname + if (!user_name) { + const user = data.bot.gml.get(data.group_id)?.get(data.user_id) || data.bot.fl.get(data.user_id) + if (user) user_name = user?.card || user?.nickname + } + Bot.makeLog("info", `群消息:${user_name ? `[${group_name ? `${group_name}, ` : ""}${user_name}] ` : ""}${data.raw_message}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + break + } case "guild": + data.message_type = "group" + data.group_id = `${data.guild_id}-${data.channel_id}` + Bot.makeLog("info", `频道消息:[${data.sender.nickname}] ${JSON.stringify(data.message)}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + Object.defineProperty(data, "friend", { get() { return this.member || {}}}) + break + default: + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) + } + + Bot.em(`${data.post_type}.${data.message_type}.${data.sub_type}`, data) + } + + async makeNotice(data) { + switch (data.notice_type) { + case "friend_recall": + Bot.makeLog("info", `好友消息撤回:${data.message_id}`, `${data.self_id} <= ${data.user_id}`) + break + case "group_recall": + Bot.makeLog("info", `群消息撤回:${data.operator_id} => ${data.user_id} ${data.message_id}`, `${data.self_id} <= ${data.group_id}`) + break + case "group_increase": + Bot.makeLog("info", `群成员增加:${data.operator_id} => ${data.user_id} ${data.sub_type}`, `${data.self_id} <= ${data.group_id}`) + if (data.user_id == data.self_id) + data.bot.getGroupMemberMap() + else + data.bot.pickGroup(data.group_id).getMemberMap() + break + case "group_decrease": + Bot.makeLog("info", `群成员减少:${data.operator_id} => ${data.user_id} ${data.sub_type}`, `${data.self_id} <= ${data.group_id}`) + if (data.user_id == data.self_id) + data.bot.getGroupMemberMap() + else + data.bot.pickGroup(data.group_id).getMemberMap() + break + case "group_admin": + Bot.makeLog("info", `群管理员变动:${data.sub_type}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + data.set = data.sub_type == "set" + break + case "group_upload": + Bot.makeLog("info", `群文件上传:${JSON.stringify(data.file)}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + break + case "group_ban": + Bot.makeLog("info", `群禁言:${data.operator_id} => ${data.user_id} ${data.sub_type} ${data.duration}秒`, `${data.self_id} <= ${data.group_id}`) + break + case "friend_add": + Bot.makeLog("info", "好友添加", `${data.self_id} <= ${data.user_id}`) + data.bot.getFriendMap() + break + case "notify": + if (data.group_id) + data.notice_type = "group" + else + data.notice_type = "friend" + switch (data.sub_type) { + case "poke": + data.operator_id = data.user_id + if (data.group_id) + Bot.makeLog("info", `群戳一戳:${data.operator_id} => ${data.target_id}`, `${data.self_id} <= ${data.group_id}`) + else + Bot.makeLog("info", `好友戳一戳:${data.operator_id} => ${data.target_id}`, data.self_id) + break + case "honor": + Bot.makeLog("info", `群荣誉:${data.honor_type}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + break + case "title": + Bot.makeLog("info", `群头衔:${data.title}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + break + default: + Bot.makeLog("warn", `未知通知:${logger.magenta(data.raw)}`, data.self_id) + } + break + case "group_card": + Bot.makeLog("info", `群名片更新:${data.card_old} => ${data.card_new}`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + break + case "offline_file": + Bot.makeLog("info", `离线文件:${JSON.stringify(data.file)}`, `${data.self_id} <= ${data.user_id}`) + break + case "client_status": + Bot.makeLog("info", `客户端${data.online ? "上线" : "下线"}:${JSON.stringify(data.client)}`, data.self_id) + data.clients = (await data.bot.sendApi("get_online_clients")).clients + data.bot.clients = data.clients + break + case "essence": + data.notice_type = "group_essence" + Bot.makeLog("info", `群精华消息:${data.operator_id} => ${data.sender_id} ${data.sub_type} ${data.message_id}`, `${data.self_id} <= ${data.group_id}`) + break + case "guild_channel_recall": + Bot.makeLog("info", `频道消息撤回:${data.operator_id} => ${data.user_id} ${data.message_id}`, `${data.self_id} <= ${data.guild_id}-${data.channel_id}`) + break + case "message_reactions_updated": + data.notice_type = "guild_message_reactions_updated" + Bot.makeLog("info", `频道消息表情贴:${data.message_id} ${JSON.stringify(data.current_reactions)}`, `${data.self_id} <= ${data.guild_id}-${data.channel_id}, ${data.user_id}`) + break + case "channel_updated": + data.notice_type = "guild_channel_updated" + Bot.makeLog("info", `子频道更新:${JSON.stringify(data.old_info)} => ${JSON.stringify(data.new_info)}`, `${data.self_id} <= ${data.guild_id}-${data.channel_id}, ${data.user_id}`) + break + case "channel_created": + data.notice_type = "guild_channel_created" + Bot.makeLog("info", `子频道创建:${JSON.stringify(data.channel_info)}`, `${data.self_id} <= ${data.guild_id}-${data.channel_id}, ${data.user_id}`) + data.bot.getGroupMap() + break + case "channel_destroyed": + data.notice_type = "guild_channel_destroyed" + Bot.makeLog("info", `子频道删除:${JSON.stringify(data.channel_info)}`, `${data.self_id} <= ${data.guild_id}-${data.channel_id}, ${data.user_id}`) + data.bot.getGroupMap() + break + default: + Bot.makeLog("warn", `未知通知:${logger.magenta(data.raw)}`, data.self_id) + } + + let notice = data.notice_type.split("_") + data.notice_type = notice.shift() + notice = notice.join("_") + if (notice) + data.sub_type = notice + + if (data.guild_id && data.channel_id) { + data.group_id = `${data.guild_id}-${data.channel_id}` + Object.defineProperty(data, "friend", { get() { return this.member || {}}}) + } + + Bot.em(`${data.post_type}.${data.notice_type}.${data.sub_type}`, data) + } + + makeRequest(data) { + switch (data.request_type) { + case "friend": + Bot.makeLog("info", `加好友请求:${data.comment}(${data.flag})`, `${data.self_id} <= ${data.user_id}`) + data.sub_type = "add" + data.approve = approve => data.bot.setFriendAddRequest(data.flag, approve) + break + case "group": + Bot.makeLog("info", `加群请求:${data.sub_type} ${data.comment}(${data.flag})`, `${data.self_id} <= ${data.group_id}, ${data.user_id}`) + data.approve = approve => data.bot.setGroupAddRequest(data.flag, data.sub_type, approve) + break + default: + Bot.makeLog("warn", `未知请求:${logger.magenta(data.raw)}`, data.self_id) + } + + data.bot.request_list.push(data) + Bot.em(`${data.post_type}.${data.request_type}.${data.sub_type}`, data) + } + + heartbeat(data) { + if (data.status) + Object.assign(data.bot.stat, data.status) + } + + makeMeta(data, ws) { + switch (data.meta_event_type) { + case "heartbeat": + this.heartbeat(data) + break + case "lifecycle": + this.connect(data, ws) + break + default: + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) + } + } + + message(data, ws) { + try { + data = { + ...JSON.parse(data), + raw: Bot.String(data), + } + } catch (err) { + return Bot.makeLog("error", ["解码数据失败", data, err]) + } + + if (data.post_type) { + if (data.meta_event_type != "lifecycle" && !Bot.uin.includes(data.self_id)) { + Bot.makeLog("warn", `找不到对应Bot,忽略消息:${logger.magenta(data.raw)}`, data.self_id) + return false + } + data.bot = Bot[data.self_id] + + switch (data.post_type) { + case "meta_event": + this.makeMeta(data, ws) + break + case "message": + this.makeMessage(data) + break + case "notice": + this.makeNotice(data) + break + case "request": + this.makeRequest(data) + break + case "message_sent": + data.post_type = "message" + this.makeMessage(data) + break + default: + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) + } + } else if (data.echo) { + Bot.emit(data.echo, data) + } else { + Bot.makeLog("warn", `未知消息:${logger.magenta(data.raw)}`, data.self_id) + } + } + + load() { + for (const i of [this.path, "go-cqhttp"]) { + if (!Array.isArray(Bot.wsf[i])) + Bot.wsf[i] = [] + Bot.wsf[i].push((ws, ...args) => + ws.on("message", data => this.message(data, ws, ...args)) + ) + } + } +}) \ No newline at end of file diff --git "a/Yunzai/plugins/example/\344\270\273\345\212\250\345\244\215\350\257\273.js" "b/Yunzai/plugins/example/\344\270\273\345\212\250\345\244\215\350\257\273.js" index f57cfa8815c6b98daa06b60804abc69c04261086..ac69b5a84a09adbd144964164211dbe8c860aa5b 100644 --- "a/Yunzai/plugins/example/\344\270\273\345\212\250\345\244\215\350\257\273.js" +++ "b/Yunzai/plugins/example/\344\270\273\345\212\250\345\244\215\350\257\273.js" @@ -1,37 +1,36 @@ -import plugin from '../../lib/plugins/plugin.js' - export class example2 extends plugin { - constructor () { + constructor() { super({ - name: '复读', - dsc: '复读用户发送的内容,然后撤回', + name: "复读", + dsc: "复读用户发送的内容,然后撤回", /** https://oicqjs.github.io/oicq/#events */ - event: 'message', + event: "message", priority: 5000, rule: [ { /** 命令正则匹配 */ - reg: '^#复读$', + reg: "^#复读$", /** 执行方法 */ - fnc: 'repeat' + fnc: "repeat", + permission: "master", } ] }) } /** 复读 */ - async repeat () { + async repeat() { /** 设置上下文,后续接收到内容会执行doRep方法 */ - this.setContext('doRep') + this.setContext("doRep") /** 回复 */ - await this.reply('请发送要复读的内容', false, { at: true }) + await this.reply("请发送要复读的内容", false, { at: true }) } /** 接受内容 */ - doRep () { + doRep() { /** 复读内容 */ this.reply(this.e.message, false, { recallMsg: 5 }) /** 结束上下文 */ - this.finish('doRep') + this.finish("doRep") } -} +} \ No newline at end of file diff --git "a/Yunzai/plugins/example/\350\277\233\347\276\244\351\200\200\347\276\244\351\200\232\347\237\245.js" "b/Yunzai/plugins/example/\350\277\233\347\276\244\351\200\200\347\276\244\351\200\232\347\237\245.js" index e80ec3041a27cb990620efc792d804dfa175df48..2853ee9fd9461cb401b5cce8c5e500c3c22e334e 100644 --- "a/Yunzai/plugins/example/\350\277\233\347\276\244\351\200\200\347\276\244\351\200\232\347\237\245.js" +++ "b/Yunzai/plugins/example/\350\277\233\347\276\244\351\200\200\347\276\244\351\200\232\347\237\245.js" @@ -1,12 +1,9 @@ -import plugin from '../../lib/plugins/plugin.js' export class newcomer extends plugin { constructor() { super({ - name: '欢迎新人', - dsc: '新人入群欢迎', - /** https://oicqjs.github.io/oicq/#events */ - event: 'notice.group.increase', - priority: 5000 + name: "欢迎新人", + dsc: "新人入群欢迎", + event: "notice.group.increase", }) } @@ -15,14 +12,14 @@ export class newcomer extends plugin { if (this.e.user_id == this.e.self_id) return /** 定义入群欢迎内容 */ - let msg = '欢迎新人!' + let msg = "欢迎新人!" /** 冷却cd 30s */ let cd = 30 /** cd */ let key = `Yz:newcomers:${this.e.group_id}` if (await redis.get(key)) return - redis.set(key, '1', { EX: cd }) + redis.set(key, "1", { EX: cd }) /** 回复 */ await this.reply([ @@ -36,13 +33,13 @@ export class newcomer extends plugin { export class outNotice extends plugin { constructor() { super({ - name: '退群通知', - dsc: 'xx退群了', - event: 'notice.group.decrease' + name: "退群通知", + dsc: "xx退群了", + event: "notice.group.decrease" }) /** 退群提示词 */ - this.tips = '退群了' + this.tips = "退群了" } async accept() { diff --git a/Yunzai/plugins/other/install.js b/Yunzai/plugins/other/install.js index d01dbf42c8eff05ce888004b24eda7d3e7011cda..2f734ad27c02b8c54e227a478768386fe6f3308e 100644 --- a/Yunzai/plugins/other/install.js +++ b/Yunzai/plugins/other/install.js @@ -1,13 +1,12 @@ -import { exec, execSync } from "child_process" -import plugin from "../../lib/plugins/plugin.js" -import fs from "node:fs" import { Restart } from "./restart.js" let insing = false const list = { "Atlas":"https://gitee.com/Nwflower/atlas", + "genshin" :"https://gitee.com/TimeRainStarSky/Yunzai-genshin", "ws-plugin":"https://gitee.com/xiaoye12123/ws-plugin", "TRSS-Plugin" :"https://Yunzai.TRSS.me", + "miao-plugin" :"https://gitee.com/yoimiya-kokomi/miao-plugin", "yenai-plugin" :"https://gitee.com/yeyang52/yenai-plugin", "flower-plugin" :"https://gitee.com/Nwflower/flower-plugin", "xianyu-plugin" :"https://gitee.com/suancaixianyu/xianyu-plugin", @@ -15,14 +14,13 @@ const list = { "useless-plugin":"https://gitee.com/SmallK111407/useless-plugin", "StarRail-plugin" :"https://gitee.com/hewang1an/StarRail-plugin", "xiaoyao-cvs-plugin":"https://gitee.com/Ctrlcvs/xiaoyao-cvs-plugin", - "Jinmaocuicuisha-plugin":"https://gitee.com/JMCCS/jinmaocuicuisha", "trss-xianxin-plugin" :"https://gitee.com/snowtafir/xianxin-plugin", - "mysVilla-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-mysVilla-Plugin", + "Lagrange-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-Lagrange-Plugin", "Telegram-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-Telegram-Plugin", "Discord-Plugin":"https://gitee.com/TimeRainStarSky/Yunzai-Discord-Plugin", - "QQGuild-Plugin":"https://gitee.com/TimeRainStarSky/Yunzai-QQGuild-Plugin", "WeChat-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-WeChat-Plugin", - "Proxy-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-Proxy-Plugin", + "QQBot-Plugin":"https://gitee.com/TimeRainStarSky/Yunzai-QQBot-Plugin", + "Route-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-Route-Plugin", "ICQQ-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-ICQQ-Plugin", "KOOK-Plugin" :"https://gitee.com/TimeRainStarSky/Yunzai-KOOK-Plugin", } @@ -53,7 +51,7 @@ export class install extends plugin { if (name == "插件") { let msg = "\n" for (const name in list) - if (!fs.existsSync(`plugins/${name}`)) + if (!await Bot.fsStat(`plugins/${name}`)) msg += `${name}\n` if (msg == "\n") @@ -66,7 +64,7 @@ export class install extends plugin { } const path = `plugins/${name}` - if (fs.existsSync(path)) { + if (await Bot.fsStat(path)) { await this.reply(`${name} 插件已安装`) return false } @@ -74,23 +72,15 @@ export class install extends plugin { this.restart() } - async execSync(cmd) { - return new Promise(resolve => { - exec(cmd, (error, stdout, stderr) => { - resolve({ error, stdout, stderr }) - }) - }) - } - async runInstall(name, url, path) { logger.mark(`${this.e.logFnc} 开始安装:${name} 插件`) await this.reply(`开始安装 ${name} 插件`) const cm = `git clone --depth 1 --single-branch "${url}" "${path}"` insing = true - const ret = await this.execSync(cm) - if (fs.existsSync(`${path}/package.json`)) - await this.execSync("pnpm install") + const ret = await Bot.exec(cm) + if (await Bot.fsStat(`${path}/package.json`)) + await Bot.exec("pnpm install") insing = false if (ret.error) { diff --git a/Yunzai/plugins/other/restart.js b/Yunzai/plugins/other/restart.js index f0ef2bd228fab79e6a5de476bd0842e999060c06..bd65dfbcae1c38067e5167edccd260623d8eafbc 100644 --- a/Yunzai/plugins/other/restart.js +++ b/Yunzai/plugins/other/restart.js @@ -1,122 +1,101 @@ -import plugin from '../../lib/plugins/plugin.js' -import { createRequire } from 'module' - -const require = createRequire(import.meta.url) -const { exec } = require('child_process') +import cfg from "../../lib/config/config.js" +import { spawn } from "child_process" export class Restart extends plugin { - constructor (e = '') { + constructor (e = "") { super({ - name: '重启', - dsc: '#重启', - event: 'message', + name: "重启", + dsc: "#重启", + event: "message", priority: 10, - rule: [{ - reg: '^#重启$', - fnc: 'restart', - permission: 'master' - }, { - reg: '^#(停机|关机)$', - fnc: 'stop', - permission: 'master' - }] + rule: [ + { + reg: "^#重启$", + fnc: "restart", + permission: "master" + }, + { + reg: "^#(停机|关机)$", + fnc: "stop", + permission: "master" + } + ] }) if (e) this.e = e + this.key = "Yz:restart" + } - this.key = 'Yz:restart' + init() { + Bot.once("online", () => this.restartMsg()) + if (cfg.bot.restart_time) { + this.e = { + logFnc: "[自动重启]" , + reply: msg => Bot.sendMasterMsg(msg), + } + setTimeout(() => this.restart(), cfg.bot.restart_time*60000) + } } - async init () { + async restartMsg() { let restart = await redis.get(this.key) - if (restart) { - restart = JSON.parse(restart) - let time = restart.time || new Date().getTime() - time = (new Date().getTime() - time) / 1000 - - let msg = `重启成功:耗时${time.toFixed(2)}秒` - + if (!restart) return + restart = JSON.parse(restart) + const time = (Date.now() - (restart.time || Date.now()))/1000 + const msg = [] + if (restart.msg_id) + msg.push(segment.reply(restart.msg_id)) + if (restart.isStop) + msg.push(`开机成功,距离上次关机${time}秒`) + else + msg.push(`重启成功,用时${time}秒`) + + if (restart.id) { if (restart.isGroup) Bot.sendGroupMsg(restart.bot_id, restart.id, msg) else Bot.sendFriendMsg(restart.bot_id, restart.id, msg) - - redis.del(this.key) + } else { + Bot.sendMasterMsg(msg) } + redis.del(this.key) } - async restart () { - await this.e.reply('开始执行重启,请稍等...') - logger.mark(`${this.e.logFnc} 开始执行重启,请稍等...`) - - let data = JSON.stringify({ + async restart() { + await this.e.reply(`开始重启,本次运行时长:${Bot.getTimeDiff()}`) + await redis.set(this.key, JSON.stringify({ isGroup: !!this.e.isGroup, id: this.e.isGroup ? this.e.group_id : this.e.user_id, bot_id: this.e.self_id, - time: new Date().getTime() - }) - - let npm = await this.checkPnpm() - - try { - await redis.set(this.key, data, { EX: 120 }) - let cm = `${npm} start` - if (process.argv[1].includes('pm2')) { - cm = `${npm} run restart` - } - - exec(cm, { windowsHide: true }, (error, stdout, stderr) => { - if (error) { - redis.del(this.key) - this.e.reply(`操作失败!\n${error.stack}`) - logger.error(`重启失败\n${error.stack}`) - } else if (stdout) { - logger.mark('重启成功,运行已由前台转为后台') - logger.mark(`查看日志请用命令:${npm} run log`) - logger.mark(`停止后台运行命令:${npm} stop`) - process.exit() - } - }) - } catch (error) { - redis.del(this.key) - let e = error.stack ?? error - this.e.reply(`操作失败!\n${e}`) + msg_id: this.e.message_id, + time: Date.now(), + })) + + if (process.env.app_type == "pm2") { + const ret = await Bot.exec("pnpm run restart") + if (!ret.error) process.exit() + await this.e.reply(`重启错误\n${ret.error}`) + Bot.makeLog("error", ["重启错误", ret]) } - - return true - } - - async checkPnpm () { - let npm = 'npm' - let ret = await this.execSync('pnpm -v') - if (ret.stdout) npm = 'pnpm' - return npm - } - - async execSync (cmd) { - return new Promise((resolve, reject) => { - exec(cmd, { windowsHide: true }, (error, stdout, stderr) => { - resolve({ error, stdout, stderr }) - }) - }) + process.exit() } - async stop () { - if (!process.argv[1].includes('pm2')) { - logger.mark('关机成功,已停止运行') - await this.e.reply('关机成功,已停止运行') - process.exit() + async stop() { + await this.e.reply(`开始关机,本次运行时长:${Bot.getTimeDiff()}`) + await redis.set(this.key, JSON.stringify({ + isStop: true, + isGroup: !!this.e.isGroup, + id: this.e.isGroup ? this.e.group_id : this.e.user_id, + bot_id: this.e.self_id, + msg_id: this.e.message_id, + time: Date.now(), + })) + + if (process.env.app_type == "pm2") { + const ret = await Bot.exec("pnpm stop") + await this.e.reply(`关机错误\n${ret.error}\n${ret.stdout}\n${ret.stderr}`) + Bot.makeLog("error", ["关机错误", ret]) } - - logger.mark('关机成功,已停止运行') - await this.e.reply('关机成功,已停止运行') - - let npm = await this.checkPnpm() - exec(`${npm} stop`, { windowsHide: true }, (error, stdout, stderr) => { - if (error) { - this.e.reply(`操作失败!\n${error.stack}`) - logger.error(`关机失败\n${error.stack}`) - } - }) + process.exit(1) } } \ No newline at end of file diff --git a/Yunzai/plugins/other/sendLog.js b/Yunzai/plugins/other/sendLog.js index 9b7d83b2c2d6bb4793da8dab1c2065c28bb35652..5f0a319eab26a78e3ebf5cf78e8d3d045ff74d20 100644 --- a/Yunzai/plugins/other/sendLog.js +++ b/Yunzai/plugins/other/sendLog.js @@ -1,6 +1,4 @@ -import plugin from "../../lib/plugins/plugin.js" -import common from "../../lib/common/common.js" -import fs from "node:fs" +import fs from "node:fs/promises" import lodash from "lodash" import moment from "moment" @@ -43,16 +41,16 @@ export class sendLog extends plugin { if (this.keyWord) type = this.keyWord - const log = this.getLog(logFile) + const log = await this.getLog(logFile) if (lodash.isEmpty(log)) return this.reply(`暂无相关日志:${type}`) - return this.reply(await common.makeForwardMsg(this.e, [log.join("\n")], `最近${log.length}条${type}日志`)) + return this.reply(await Bot.makeForwardArray([`最近${log.length}条${type}日志`, log.join("\n")])) } - getLog(logFile) { - let log = fs.readFileSync(logFile, { encoding: "utf-8" }) + async getLog(logFile) { + let log = await fs.readFile(logFile, "utf-8") log = log.split("\n") if (this.keyWord) { diff --git a/Yunzai/plugins/other/update.js b/Yunzai/plugins/other/update.js index a33c30fcd613cbd39ce1a47037b319ebd2d7dcd3..9b0db7b446cf231070e23c299cc66b1f6c73c3d0 100644 --- a/Yunzai/plugins/other/update.js +++ b/Yunzai/plugins/other/update.js @@ -1,12 +1,7 @@ -import plugin from '../../lib/plugins/plugin.js' -import { createRequire } from 'module' +import cfg from '../../lib/config/config.js' import lodash from 'lodash' -import fs from 'node:fs' +import fs from 'node:fs/promises' import { Restart } from './restart.js' -import common from '../../lib/common/common.js' - -const require = createRequire(import.meta.url) -const { exec, execSync } = require('child_process') let uping = false @@ -37,6 +32,25 @@ export class update extends plugin { this.typeName = 'TRSS-Yunzai' } + init() { + if (cfg.bot.update_time) { + this.e = { + isMaster: true, + logFnc: "[自动更新]", + msg: "#全部更新", + reply: msg => Bot.sendMasterMsg(msg), + } + this.autoUpdate() + } + } + + autoUpdate() { + setTimeout(() => { + this.updateAll() + this.autoUpdate() + }, cfg.bot.update_time*60000) + } + async update() { if (!this.e.isMaster) return false if (uping) return this.reply('已有命令更新中..请勿重复操作') @@ -44,39 +58,29 @@ export class update extends plugin { if (/详细|详情|面板|面版/.test(this.e.msg)) return false /** 获取插件 */ - const plugin = this.getPlugin() + let plugin = await this.getPlugin() if (plugin === false) return false - /** 执行更新 */ await this.runUpdate(plugin) - /** 是否需要重启 */ - if (this.isUp) { - // await this.reply('即将执行重启,以应用更新') - setTimeout(() => this.restart(), 2000) - } + if (this.isPkgUp) + await Bot.exec("pnpm install") + if (this.isUp) + this.restart() } - getPlugin(plugin = '') { + async getPlugin(plugin = '') { if (!plugin) { plugin = this.e.msg.replace(/#(强制)?更新(日志)?/, '') if (!plugin) return '' } - if (!fs.existsSync(`plugins/${plugin}/.git`)) return false + if (!await Bot.fsStat(`plugins/${plugin}/.git`)) return false this.typeName = plugin return plugin } - async execSync(cmd) { - return new Promise((resolve, reject) => { - exec(cmd, { windowsHide: true }, (error, stdout, stderr) => { - resolve({ error, stdout, stderr }) - }) - }) - } - async runUpdate(plugin = '') { this.isNowUp = false @@ -95,22 +99,24 @@ export class update extends plugin { await this.reply(`开始${type} ${this.typeName}`) uping = true - const ret = await this.execSync(cm) + const ret = await Bot.exec(cm) uping = false + ret.stdout = String(ret.stdout) if (ret.error) { logger.mark(`${this.e.logFnc} 更新失败:${this.typeName}`) - this.gitErr(ret.error, ret.stdout) + this.gitErr(Bot.String(ret.error), ret.stdout) return false } const time = await this.getTime(plugin) - if (/Already up|已经是最新/g.test(ret.stdout)) { await this.reply(`${this.typeName} 已是最新\n最后更新时间:${time}`) } else { - await this.reply(`${this.typeName} 更新成功\n更新时间:${time}`) this.isUp = true + if (/package\.json/.test(ret.stdout)) + this.isPkgUp = true + await this.reply(`${this.typeName} 更新成功\n更新时间:${time}`) await this.reply(await this.getLog(plugin)) } @@ -121,69 +127,56 @@ export class update extends plugin { async getcommitId(plugin = '') { let cm = 'git rev-parse --short HEAD' if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` - - const commitId = await execSync(cm, { encoding: 'utf-8' }) - return lodash.trim(commitId) + cm = await Bot.exec(cm) + return lodash.trim(String(cm.stdout)) } async getTime(plugin = '') { let cm = 'git log -1 --pretty=%cd --date=format:"%F %T"' if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` - - let time = '' - try { - time = await execSync(cm, { encoding: 'utf-8' }) - time = lodash.trim(time) - } catch (error) { - logger.error(error.toString()) - time = '获取时间失败' - } - - return time + cm = await Bot.exec(cm) + return lodash.trim(String(cm.stdout)) } - async gitErr(err, stdout) { + async gitErr(error, stdout) { const msg = '更新失败!' - const errMsg = err.toString() - stdout = stdout.toString() - if (errMsg.includes('Timed out')) { - const remote = errMsg.match(/'(.+?)'/g)[0].replace(/'/g, '') + if (error.includes('Timed out')) { + const remote = error.match(/'(.+?)'/g)[0].replace(/'/g, '') return this.reply(`${msg}\n连接超时:${remote}`) } - if (/Failed to connect|unable to access/g.test(errMsg)) { - const remote = errMsg.match(/'(.+?)'/g)[0].replace(/'/g, '') + if (/Failed to connect|unable to access/g.test(error)) { + const remote = error.match(/'(.+?)'/g)[0].replace(/'/g, '') return this.reply(`${msg}\n连接失败:${remote}`) } - if (errMsg.includes('be overwritten by merge')) { - return this.reply(`${msg}\n存在冲突:\n${errMsg}\n请解决冲突后再更新,或者执行#强制更新,放弃本地修改`) + if (error.includes('be overwritten by merge')) { + return this.reply(`${msg}\n存在冲突:\n${error}\n请解决冲突后再更新,或者执行#强制更新,放弃本地修改`) } if (stdout.includes('CONFLICT')) { - return this.reply(`${msg}\n存在冲突:\n${errMsg}${stdout}\n请解决冲突后再更新,或者执行#强制更新,放弃本地修改`) + return this.reply(`${msg}\n存在冲突:\n${error}${stdout}\n请解决冲突后再更新,或者执行#强制更新,放弃本地修改`) } - return this.reply([errMsg, stdout]) + return this.reply([error, stdout]) } async updateAll() { - const dirs = fs.readdirSync('./plugins/') + const dirs = await fs.readdir('./plugins/') await this.runUpdate() for (let plu of dirs) { - plu = this.getPlugin(plu) + plu = await this.getPlugin(plu) if (plu === false) continue - await common.sleep(1500) await this.runUpdate(plu) } - if (this.isUp) { - // await this.reply('即将执行重启,以应用更新') - setTimeout(() => this.restart(), 2000) - } + if (this.isPkgUp) + await Bot.exec("pnpm install") + if (this.isUp) + this.restart() } restart() { @@ -194,17 +187,14 @@ export class update extends plugin { let cm = 'git log -100 --pretty="%h||[%cd] %s" --date=format:"%F %T"' if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` - let logAll - try { - logAll = await execSync(cm, { encoding: 'utf-8' }) - } catch (error) { - logger.error(error.toString()) - await this.reply(error.toString()) + cm = await Bot.exec(cm) + if (cm.error) { + logger.error(cm.error) + await this.reply(String(cm.error)) } + const logAll = String(cm.stdout).trim().split('\n') - if (!logAll) return false - - logAll = logAll.trim().split('\n') + if (!logAll.length) return false let log = [] for (let str of logAll) { @@ -218,23 +208,21 @@ export class update extends plugin { if (log.length <= 0) return '' - let end = '' - try { - cm = 'git config -l' - if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` - end = await execSync(cm, { encoding: 'utf-8' }) - end = end.match(/remote\..*\.url=.+/g).join('\n\n').replace(/remote\..*\.url=/g, '').replace(/\/\/([^@]+)@/, '//') - } catch (error) { - logger.error(error.toString()) - await this.reply(error.toString()) + cm = 'git config -l' + if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` + cm = await Bot.exec(cm) + const end = String(cm.stdout).match(/remote\..*\.url=.+/g).join('\n\n').replace(/remote\..*\.url=/g, '').replace(/\/\/([^@]+)@/, '//') + if (cm.error) { + logger.error(cm.error) + await this.reply(String(cm.error)) } - return common.makeForwardMsg(this.e, [log, end], `${plugin || 'TRSS-Yunzai'} 更新日志,共${line}条`) + return Bot.makeForwardArray([`${plugin || 'TRSS-Yunzai'} 更新日志,共${line}条`, log, end]) } async updateLog() { - const plugin = this.getPlugin() + const plugin = await this.getPlugin() if (plugin === false) return false return this.reply(await this.getLog(plugin)) } -} \ No newline at end of file +} diff --git a/Yunzai/plugins/other/version.js b/Yunzai/plugins/other/version.js index f77786549d97f6c47b41612ae675b9198de3872b..19f0d640ef42d4374f44cc10bbce8cb82e2c3130 100644 --- a/Yunzai/plugins/other/version.js +++ b/Yunzai/plugins/other/version.js @@ -1,27 +1,35 @@ -import { App, Common, Version } from '#miao' +let App, Common, Version +try { + App = (await import("#miao")).App + Common = (await import("#miao")).Common + Version = (await import("#miao")).Version +} catch (err) {} -let app = App.init({ - id: 'version', - name: '版本', - desc: '版本' -}) +export let version = {} +if (App) { + let app = App.init({ + id: "version", + name: "版本", + desc: "版本" + }) -app.reg({ - version: { - rule: /^#版本$/, - desc: '【#帮助】 版本介绍', - fn: async function (e) { - let { changelogs, currentVersion } = Version.readLogFile('root') - return await Common.render('help/version-info', { - currentVersion, - changelogs, - name: 'TRSS-Yunzai', - elem: 'cryo', - pluginName: false, - pluginVersion: false - }, { e, scale: 1.2 }) + app.reg({ + version: { + rule: /^#版本$/, + desc: "【#帮助】 版本介绍", + fn: async function (e) { + let { changelogs, currentVersion } = Version.readLogFile("root") + return await Common.render("help/version-info", { + currentVersion, + changelogs, + name: "TRSS-Yunzai", + elem: "cryo", + pluginName: false, + pluginVersion: false, + }, { e, scale: 1.2 }) + } } - } -}) + }) -export const version = app.v3App() + version = app.v3App() +} \ No newline at end of file diff --git a/Yunzai/plugins/system/add.js b/Yunzai/plugins/system/add.js index eacf1ac98268bd8dc9e89ddd044047dfe21c4121..4f358ed89a7643c529455307bbe3f361a5730864 100644 --- a/Yunzai/plugins/system/add.js +++ b/Yunzai/plugins/system/add.js @@ -1,11 +1,7 @@ import cfg from "../../lib/config/config.js" -import plugin from "../../lib/plugins/plugin.js" -import common from "../../lib/common/common.js" -import fs from "node:fs" +import fs from "node:fs/promises" import path from "node:path" import lodash from "lodash" -import fetch from "node-fetch" -import { fileTypeFromBuffer } from "file-type" let messageMap = {} @@ -41,7 +37,7 @@ export class add extends plugin { } async init() { - common.mkdirs(this.path) + await Bot.mkdir(this.path) } /** 群号key */ @@ -59,7 +55,7 @@ export class add extends plugin { return } - this.initMessageMap() + await this.initMessageMap() if (!this.checkAuth()) return false /** 获取关键词 */ @@ -101,6 +97,10 @@ export class add extends plugin { checkAuth() { if (this.e.isMaster) return true + if (this.isGlobal) { + this.reply("暂无权限,只有主人才能操作") + return false + } const groupCfg = cfg.getGroup(this.e.self_id, this.group_id) if (groupCfg.addLimit == 2) { @@ -145,7 +145,7 @@ export class add extends plugin { /** 添加内容 */ async addContext() { - const context = this.getContext()?.addContext + const context = this.getContext("addContext") this.isGlobal = context.isGlobal await this.getGroupId() /** 关键词 */ @@ -181,46 +181,25 @@ export class add extends plugin { if (message.length > 1) this.keyWord += String(message.length) - this.saveJson() + await this.saveJson() return this.reply(`添加成功:${this.keyWord}`) } - saveJson() { + async saveJson() { let obj = {} for (let [k, v] of messageMap[this.group_id]) obj[k] = v - fs.writeFileSync(`${this.path}${this.group_id}.json`, JSON.stringify(obj, "", "\t")) - } - - async makeBuffer(file) { - if (file.match(/^base64:\/\//)) - return Buffer.from(file.replace(/^base64:\/\//, ""), "base64") - else if (file.match(/^https?:\/\//)) - return Buffer.from(await (await fetch(file)).arrayBuffer()) - else if (fs.existsSync(file)) - return Buffer.from(fs.readFileSync(file)) - return file - } - - async fileType(data) { - const file = { name: `${this.group_id}/${data.type}/${Date.now()}` } - try { - file.url = data.url.replace(/^base64:\/\/.*/, "base64://...") - file.buffer = await this.makeBuffer(data.url) - file.type = await fileTypeFromBuffer(file.buffer) - file.name = `${file.name}.${file.type.ext}` - } catch (err) { - logger.error(`文件类型检测错误:${logger.red(err)}`) - file.name = `${file.name}-${path.basename(data.file || data.url)}` - } - return file + await fs.writeFile(`${this.path}${this.group_id}.json`, JSON.stringify(obj, "", "\t")) } async saveFile(data) { - const file = await this.fileType(data) - if (file.name && Buffer.isBuffer(file.buffer) && common.mkdirs(path.dirname(`${this.path}${file.name}`))) { - fs.writeFileSync(`${this.path}${file.name}`, file.buffer) + const file = await Bot.fileType({ ...data, file: data.url }) + if (Buffer.isBuffer(file.buffer)) { + file.name = `${this.group_id}/${data.type}/${file.name}` + file.path = `${this.path}${file.name}` + await Bot.mkdir(path.dirname(file.path)) + await fs.writeFile(file.path, file.buffer) return file.name } return data.url @@ -233,8 +212,8 @@ export class add extends plugin { await this.getGroupId() if (!this.group_id) return false - this.initMessageMap() - this.initGlobalMessageMap() + await this.initMessageMap() + await this.initGlobalMessageMap() this.keyWord = this.trimAlias(this.e.raw_message.trim()) let keyWord = this.keyWord @@ -260,8 +239,8 @@ export class add extends plugin { msg = [...msg[num]] for (const i in msg) - if (msg[i].file && fs.existsSync(`${this.path}${msg[i].file}`)) - msg[i] = { ...msg[i], file: `base64://${fs.readFileSync(`${this.path}${msg[i].file}`).toString("base64")}` } + if (msg[i].file && await Bot.fsStat(`${this.path}${msg[i].file}`)) + msg[i] = { ...msg[i], file: `${this.path}${msg[i].file}` } logger.mark(`[发送消息]${this.e.logText} ${this.keyWord}`) const groupCfg = cfg.getGroup(this.e.self_id, this.group_id) @@ -272,15 +251,15 @@ export class add extends plugin { } /** 初始化已添加内容 */ - initMessageMap() { + async initMessageMap() { if (messageMap[this.group_id]) return messageMap[this.group_id] = new Map() const path = `${this.path}${this.group_id}.json` - if (!fs.existsSync(path)) return + if (!await Bot.fsStat(path)) return try { - const message = JSON.parse(fs.readFileSync(path, "utf8")) + const message = JSON.parse(await fs.readFile(path, "utf8")) for (const i in message) messageMap[this.group_id].set(i, message[i]) } catch (err) { @@ -289,15 +268,15 @@ export class add extends plugin { } /** 初始化全局已添加内容 */ - initGlobalMessageMap() { + async initGlobalMessageMap() { if (messageMap.global) return messageMap.global = new Map() const globalPath = `${this.path}global.json` - if (!fs.existsSync(globalPath)) return + if (!await Bot.fsStat(globalPath)) return try { - const message = JSON.parse(fs.readFileSync(globalPath, "utf8")) + const message = JSON.parse(await fs.readFile(globalPath, "utf8")) for (const i in message) messageMap.global.set(i, message[i]) } catch (err) { @@ -310,7 +289,7 @@ export class add extends plugin { await this.getGroupId() if (!(this.group_id && this.checkAuth())) return false - this.initMessageMap() + await this.initMessageMap() this.getKeyWord() if (!this.keyWord) { @@ -370,7 +349,7 @@ export class add extends plugin { } } - this.saveJson() + await this.saveJson() return this.reply(`删除成功:${this.keyWord}`) } @@ -384,7 +363,7 @@ export class add extends plugin { await this.getGroupId() if (!this.group_id) return false - this.initMessageMap() + await this.initMessageMap() const search = this.e.msg.replace(/^#(全局)?(消息|词条)/, "").trim() if (search.match(/^列表/)) @@ -426,16 +405,17 @@ export class add extends plugin { msg.push(`${i.num}. ${keyWord}(${i.val.length})`) num++ } - msg = [msg.join("\n")] - - if (type == "list" && count > 100) - msg.push(`更多内容请翻页查看\n如:#消息列表${Number(page)+1}`) let title = `消息列表:第${page}页,共${count}条` if (type == "search") title = `消息${search}:共${count}条` - return this.reply(await common.makeForwardMsg(this.e, msg, title)) + msg = [title, msg.join("\n")] + + if (type == "list" && count > 100) + msg.push(`更多内容请翻页查看\n如:#消息列表${Number(page)+1}`) + + return this.reply(await Bot.makeForwardArray(msg)) } /** 分页 */ diff --git a/Yunzai/plugins/system/botOperate.js b/Yunzai/plugins/system/botOperate.js index acceaab74024b7f87b7b4f7fc8b34f5614fc900e..e0eb62b62b07baeba38c78f2a8f82a0554246c92 100644 --- a/Yunzai/plugins/system/botOperate.js +++ b/Yunzai/plugins/system/botOperate.js @@ -1,7 +1,7 @@ export class botOperate extends plugin { - constructor () { + constructor() { super({ - name: "Bot 操作", + name: "botOperate", dsc: "Bot 操作", event: "message", rule: [ @@ -20,7 +20,10 @@ export class botOperate extends plugin { } Verify() { - const data = { msg: this.e.msg.replace(/^#(Bot|机器人)验证/, "").trim().split(":") } + const data = { + msg: this.e.msg.replace(/^#(Bot|机器人)验证/, "").trim().split(":"), + reply: msg => this.reply(msg, true), + } data.self_id = data.msg.shift() data.msg = data.msg.join(":") Bot.em(`verify.${data.self_id}`, data) diff --git a/Yunzai/plugins/system/disablePrivate.js b/Yunzai/plugins/system/disablePrivate.js index b029557a6f8623b0e08489e45e6b20104d752c75..62644717595dd60acbf1f59d6f77704793d30b30 100644 --- a/Yunzai/plugins/system/disablePrivate.js +++ b/Yunzai/plugins/system/disablePrivate.js @@ -1,5 +1,4 @@ import cfg from '../../lib/config/config.js' -import plugin from '../../lib/plugins/plugin.js' export class disPri extends plugin { constructor () { @@ -29,9 +28,14 @@ export class disPri extends plugin { /** 绑定ck,抽卡链接 */ let wordReg = /(.*)(ltoken|_MHYUUID|authkey=)(.*)|导出记录(json)*|(记录|安卓|苹果|ck|cookie|体力)帮助|^帮助$|^#*(删除|我的)ck$|^#(我的)?(uid|UID)[0-9]{0,2}$/g /** 自定义通行字符 */ - let disableReg = `(.*)(${cfg.other?.disableAdopt?.join('|')})(.*)` + let disableAdopt = cfg.other?.disableAdopt + if (!Array.isArray(disableAdopt)) { + disableAdopt = [] + } + disableAdopt = disableAdopt.filter(str => str != null && str !== ''); + let disableReg = `(.*)(${disableAdopt.join('|')})(.*)` if (this.e.raw_message) { - if (!new RegExp(wordReg).test(this.e.raw_message) && (!new RegExp(disableReg).test(this.e.raw_message))) { + if (!new RegExp(wordReg).test(this.e.raw_message) && (disableAdopt.length === 0 || !new RegExp(disableReg).test(this.e.raw_message))) { this.sendTips() return 'return' } diff --git a/Yunzai/plugins/system/friend.js b/Yunzai/plugins/system/friend.js index ce3c17b40efbf828ac9cea4589bea90795583c73..9efeae4eda44490f37a854f31a8022f7073b1272 100644 --- a/Yunzai/plugins/system/friend.js +++ b/Yunzai/plugins/system/friend.js @@ -1,20 +1,19 @@ -import cfg from '../../lib/config/config.js' -import common from '../../lib/common/common.js' +import cfg from "../../lib/config/config.js" export class friend extends plugin { constructor () { super({ - name: 'autoFriend', - dsc: '自动同意好友', - event: 'request.friend' + name: "autoFriend", + dsc: "自动同意好友", + event: "request.friend" }) } async accept() { - if (this.e.sub_type == 'add' || this.e.sub_type == 'single') { + if (this.e.sub_type == "add" || this.e.sub_type == "single") { if (cfg.other.autoFriend == 1) { logger.mark(`[自动同意][添加好友] ${this.e.user_id}`) - await common.sleep(2000) + await Bot.sleep(3000) this.e.approve(true) } } diff --git a/Yunzai/plugins/system/invite.js b/Yunzai/plugins/system/invite.js index 2b74778272e8eedf5eed47f3b0965f5a38546651..4c00b2ad0eb05189cc93e5ad542958ad27441102 100644 --- a/Yunzai/plugins/system/invite.js +++ b/Yunzai/plugins/system/invite.js @@ -1,11 +1,9 @@ -import cfg from '../../lib/config/config.js' - export class invite extends plugin { constructor () { super({ - name: 'invite', - dsc: '主人邀请自动进群', - event: 'request.group.invite' + name: "invite", + dsc: "主人邀请自动进群", + event: "request.group.invite" }) } @@ -18,4 +16,4 @@ export class invite extends plugin { this.e.approve(true) this.e.bot.pickFriend(this.e.user_id).sendMsg(`已同意加群:${this.e.group_name}`) } -} +} \ No newline at end of file diff --git a/Yunzai/plugins/system/master.js b/Yunzai/plugins/system/master.js index 8f238dce5b23ad3c79554a5341c270ecaf5873bb..569aeeec3e0c5f9f9254549018907c202efa3fce 100644 --- a/Yunzai/plugins/system/master.js +++ b/Yunzai/plugins/system/master.js @@ -1,9 +1,9 @@ -import fs from "fs" -import { randomUUID } from "crypto" +import fs from "node:fs/promises" +import { randomUUID } from "node:crypto" let code = {} let file = "config/config/other.yaml" export class master extends plugin { - constructor () { + constructor() { super({ name: "设置主人", dsc: "设置主人", @@ -17,8 +17,8 @@ export class master extends plugin { }) } - edit (file, key, value) { - let data = fs.readFileSync(file, "utf8") + async edit(file, key, value) { + let data = await fs.readFile(file, "utf8") if (data.match(RegExp(`- "?${value}"?`))) return value = `${key}:\n - "${value}"` @@ -26,27 +26,27 @@ export class master extends plugin { data = data.replace(RegExp(`${key}:`), value) else data = `${data}\n${value}` - fs.writeFileSync(file, data, "utf8") + return fs.writeFile(file, data, "utf8") } - async master () { + async master() { if (this.e.isMaster) { - await this.reply(`账号:${this.e.user_id} 已经为主人`, true) + await this.reply(`[${this.e.user_id}] 已经为主人`, true) return false } code[this.e.user_id] = randomUUID() logger.mark(`${logger.cyan(`[${this.e.user_id}]`)} 设置主人验证码:${logger.green(code[this.e.user_id])}`) this.setContext("verify") - await this.reply(`账号:${this.e.user_id} 请输入验证码`, true) + await this.reply(`[${this.e.user_id}] 请输入验证码`, true) } - async verify () { + async verify() { this.finish("verify") if (this.e.msg.trim() == code[this.e.user_id]) { - this.edit(file, "masterQQ", this.e.user_id) - this.edit(file, "master", `${this.e.self_id}:${this.e.user_id}`) - await this.reply(`账号:${this.e.user_id} 设置主人成功`, true) + await this.edit(file, "masterQQ", this.e.user_id) + await this.edit(file, "master", `${this.e.self_id}:${this.e.user_id}`) + await this.reply(`[${this.e.user_id}] 设置主人成功`, true) } else { await this.reply("验证码错误", true) return false diff --git a/Yunzai/plugins/system/quit.js b/Yunzai/plugins/system/quit.js index 6c8d7f5b8101a4c5c813babfa3bf6055337d2b49..13a0162428fce03ffb58e218c2a359e728a65396 100644 --- a/Yunzai/plugins/system/quit.js +++ b/Yunzai/plugins/system/quit.js @@ -1,36 +1,34 @@ -import cfg from '../../lib/config/config.js' - +import cfg from "../../lib/config/config.js" export class quit extends plugin { - constructor () { + constructor() { super({ - name: 'notice', - dsc: '自动退群', - event: 'notice.group.increase' + name: "notice", + dsc: "自动退群", + event: "notice.group.increase" }) } - async accept () { - if (this.e.user_id != this.e.self_id) return + async accept() { + if (this.e.user_id != this.e.self_id || !this.e.group?.quit || !this.e.group.getMemberMap) return false + + const other = cfg.other + if (!other.autoQuit) return false - let other = cfg.other - if (other.autoQuit <= 0) return + const gml = await this.e.group.getMemberMap() + if (!gml instanceof Map) return false - /** 判断主人,主人邀请不退群 */ - let gl = await this.e.group.getMemberMap() - for (let qq of cfg.masterQQ) { - if (gl.has(Number(qq) || String(qq))) { + /** 判断主人邀请不退群 */ + for (const qq of cfg.masterQQ) + if (gml.has(Number(qq) || String(qq))) { logger.mark(`[主人拉群] ${this.e.group_id}`) - return + return false } - } /** 自动退群 */ - if (Array.from(gl).length <= other.autoQuit && !this.e.group.is_owner) { - await this.e.reply('禁止拉群,已自动退出') + if (Array.from(gml).length <= other.autoQuit && !this.e.group.is_owner) { + await this.reply("禁止拉群,已自动退出") logger.mark(`[自动退群] ${this.e.group_id}`) - setTimeout(() => { - this.e.group.quit() - }, 2000) + this.e.group.quit() } } -} +} \ No newline at end of file diff --git a/Yunzai/plugins/system/status.js b/Yunzai/plugins/system/status.js index a8b9a4123728b53e2aff732f938ffdbdb834a972..d7d3e0bf01bc5be6bcc4784badaf3f364482afd0 100644 --- a/Yunzai/plugins/system/status.js +++ b/Yunzai/plugins/system/status.js @@ -1,124 +1,171 @@ -import cfg from '../../lib/config/config.js' -import moment from 'moment' +import cfg from "../../lib/config/config.js" +import moment from "moment" export class status extends plugin { constructor() { super({ - name: '其他功能', - dsc: '#状态', - event: 'message', + name: "状态统计", + dsc: "#状态", + event: "message", rule: [ { - reg: '^#状态$', - fnc: 'status' + reg: "^#(状态|统计)", + fnc: "status", } ] }) } async status() { - if (this.e.isMaster) return this.statusMaster() - if (!this.e.isGroup) return this.reply('请群聊查看') - return this.statusGroup() + if (!this.e.isMaster) + return this.reply(await this.getCount({ + "用户": this.e.user_id, + "群": this.e.group_id, + })) + + const msg = + `—— TRSS Yunzai v${cfg.package.version} ——\n`+ + `运行时间:${Bot.getTimeDiff()}\n`+ + `内存使用:${(process.memoryUsage().rss/1024/1024).toFixed(2)}MB\n`+ + `系统版本:${process.platform} ${process.arch} ${process.version}`+ + this.botTime() + await this.count() + + return this.reply(msg) } - async statusMaster() { - let runTime = moment().diff(moment.unix(this.e.bot.stat.start_time), 'seconds') - let Day = Math.floor(runTime / 3600 / 24) - let Hour = Math.floor((runTime / 3600) % 24) - let Min = Math.floor((runTime / 60) % 60) - if (Day > 0) { - runTime = `${Day}天${Hour}小时${Min}分钟` - } else { - runTime = `${Hour}小时${Min}分钟` - } - - let format = (bytes) => { - return (bytes / 1024 / 1024).toFixed(2) + 'MB' - } - - let msg = '-------状态-------' - msg += `\n运行时间:${runTime}` - msg += `\n内存使用:${format(process.memoryUsage().rss)}` - msg += `\n当前版本:v${cfg.package.version}` - msg += '\n-------累计-------' - msg += await this.getCount() - - await this.reply(msg) - } - - async statusGroup() { - let msg = '-------状态-------' - msg += await this.getCount(this.e.group_id) - - await this.reply(msg) + botTime() { + let msg = "\n\n账号在线时长" + for (const i of Bot.uin) + if (Bot[i]?.stat?.start_time) + msg += `\n${Bot.getTimeDiff(Bot[i].stat.start_time)} ${i}` + return msg } - async getCount(groupId = '') { - this.date = moment().format('MMDD') - this.month = Number(moment().month()) + 1 - - this.key = 'Yz:count:' - - if (groupId) this.key += `group:${groupId}:` - - this.msgKey = { - day: `${this.key}sendMsg:day:`, - month: `${this.key}sendMsg:month:` + count() { + const cmd = { + msg: this.e.msg.replace(/^#(状态|统计)/, "").trim().split(" ") } - - this.screenshotKey = { - day: `${this.key}screenshot:day:`, - month: `${this.key}screenshot:month:` + let key = "" + for (const i of cmd.msg) if (key) { + cmd[key] = i + key = "" + } else { + key = i } + return this.getCount(cmd) + } - let week = { - msg: 0, - screenshot: 0 + async getCount(cmd) { + const date = [] + if (cmd["日期"]) { + cmd["日期"] = cmd["日期"].replace(/[^\d]/g,"") + switch (cmd["日期"].length) { + case 8: + date.push([ + cmd["日期"].slice(0, 4), + cmd["日期"].slice(4, 6), + cmd["日期"].slice(6, 8), + ]) + break + case 4: + date.push([ + moment().format("YYYY"), + cmd["日期"].slice(0, 2), + cmd["日期"].slice(2, 4), + ]) + break + case 2: + date.push([ + moment().format("YYYY"), + moment().format("MM"), + cmd["日期"], + ]) + break + default: + this.reply(`日期格式错误:${cmd["日期"]}`) + return "" + } + } else { + const d = moment() + for (let i = 0; i < 3; i++) { + date.push(d.format("YYYY MM DD").split(" ")) + d.add(-86400000) + } + date.push( + [d.format("YYYY"), d.format("MM")], + [d.format("YYYY")], + ["total"], + ) } - for (let i = 0; i <= 6; i++) { - let date = moment().startOf('week').add(i, 'days').format('MMDD') - week.msg += Number(await redis.get(`${this.msgKey.day}${date}`)) ?? 0 - week.screenshot += Number(await redis.get(`${this.screenshotKey.day}${date}`)) ?? 0 + let msg = "消息统计" + if (cmd["消息"]) { + msg = `${cmd["消息"]} ${msg}` + } else { + cmd["消息"] = "msg" } - let count = { - total: { - msg: await redis.get(`${this.key}sendMsg:total`) || 0, - screenshot: await redis.get(`${this.key}screenshot:total`) || 0 - }, - today: { - msg: await redis.get(`${this.msgKey.day}${this.date}`) || 0, - screenshot: await redis.get(`${this.screenshotKey.day}${this.date}`) || 0 - }, - week, - month: { - msg: await redis.get(`${this.msgKey.month}${this.month}`) || 0, - screenshot: await redis.get(`${this.screenshotKey.month}${this.month}`) || 0 - } + const array = [] + if (cmd["机器人"]) + array.push({ text: "机器人", key: `bot`, id: cmd["机器人"] }) + if (cmd["用户"]) + array.push({ text: "用户", key: `user`, id: cmd["用户"] }) + if (cmd["群"]) + array.push({ text: "群", key: `group`, id: cmd["群"] }) + if (!array.length) { + array.push( + { text: msg, key: "total" }, + { type: "keys", text: "用户量", key: "user:*" }, + { type: "keys", text: "群量", key: "group:*" }, + ) + msg = "" + if (this.e.self_id) + array.push({ text: "机器人", key: `bot`, id: this.e.self_id }) + if (this.e.user_id) + array.push({ text: "用户", key: `user`, id: this.e.user_id }) + if (this.e.group_id) + array.push({ text: "群", key: `group`, id: this.e.group_id }) } - let msg = '' - if (groupId) { - msg = `\n发送消息:${count.today.msg}条` - msg += `\n生成图片:${count.today.screenshot}次` - } else { - msg = `\n发送消息:${count.total.msg}条` - msg += `\n生成图片:${count.total.screenshot}次` + for (const i of array) { + if (i.id) { + i.text += ` ${i.id}` + i.key += `:${i.id}` + } + msg += `\n\n${i.text}` + for (let d of date) { + const key = `:${cmd["消息"]}:${i.key}:${d.join(":")}` + d = d.join("-") + if (d == "total") + d = `总计 -------` + else + d = `${d} ${"-".repeat(11 - d.length)}` + const ret = await this.redis(i.type, key) + msg += `\n${d} 收 ${ret.receive} 发 ${ret.send}` + } } + return msg + } - if (count.month.msg > 200) { - msg += '\n-------本周-------' - msg += `\n发送消息:${count.week.msg}条` - msg += `\n生成图片:${count.week.screenshot}次` - } - if (moment().format('D') >= 8 && count.month.msg > 400) { - msg += '\n-------本月-------' - msg += `\n发送消息:${count.month.msg}条` - msg += `\n生成图片:${count.month.screenshot}次` + async redis(type, key) { + const ret = {} + for (const i of ["receive", "send"]) { + const k = `Yz:count:${i}${key}` + if (type == "keys") + ret[i] = await this.redisKeysLength(k) || 0 + else + ret[i] = await redis.get(k) || 0 } + return ret + } - return msg + async redisKeysLength(MATCH) { + let cursor = 0, length = 0 + do { + const reply = await redis.scan(cursor, { MATCH, COUNT: 10000 }) + cursor = reply.cursor + length += reply.keys.length + } while (cursor != 0) + return length } -} +} \ No newline at end of file diff --git a/Yunzai/renderers/puppeteer/lib/puppeteer.js b/Yunzai/renderers/puppeteer/lib/puppeteer.js index 9daf954b99dcf88cd60a8e3ea718e8f18495b7c6..95c38515800546bfc5e68fb949319a4385c3e001 100644 --- a/Yunzai/renderers/puppeteer/lib/puppeteer.js +++ b/Yunzai/renderers/puppeteer/lib/puppeteer.js @@ -1,23 +1,20 @@ -import Renderer from '../../../lib/renderer/Renderer.js' -import os from 'node:os' -import lodash from 'lodash' -import puppeteer from 'puppeteer' +import Renderer from "../../../lib/renderer/Renderer.js" +import os from "node:os" +import lodash from "lodash" +import puppeteer from "puppeteer" // 暂时保留对原config的兼容 -import cfg from '../../../lib/config/config.js' -import { Data } from '#miao' +import cfg from "../../../lib/config/config.js" const _path = process.cwd() // mac地址 -let mac = '' -// 超时计时器 -let overtimeList = [] +let mac = "" export default class Puppeteer extends Renderer { - constructor (config) { + constructor(config) { super({ - id: 'puppeteer', - type: 'image', - render: 'screenshot' + id: "puppeteer", + type: "image", + render: "screenshot" }) this.browser = false this.lock = false @@ -27,22 +24,20 @@ export default class Puppeteer extends Renderer { /** 截图次数 */ this.renderNum = 0 this.config = { - headless: Data.def(config.headless, 'new'), - args: Data.def(config.args, [ - '--disable-gpu', - '--disable-setuid-sandbox', - '--no-sandbox', - '--no-zygote' - ]) + headless: config.headless || "new", + args: config.args || [ + "--disable-gpu", + "--disable-setuid-sandbox", + "--no-sandbox", + "--no-zygote" + ] } - if (config.chromiumPath || cfg?.bot?.chromium_path) { + if (config.chromiumPath || cfg?.bot?.chromium_path) /** chromium其他路径 */ this.config.executablePath = config.chromiumPath || cfg?.bot?.chromium_path - } - if (config.puppeteerWS || cfg?.bot?.puppeteer_ws) { + if (config.puppeteerWS || cfg?.bot?.puppeteer_ws) /** chromium其他路径 */ this.config.wsEndpoint = config.puppeteerWS || cfg?.bot?.puppeteer_ws - } /** puppeteer超时超时时间 */ this.puppeteerTimeout = config.puppeteerTimeout || cfg?.bot?.puppeteer_timeout || 0 } @@ -50,12 +45,12 @@ export default class Puppeteer extends Renderer { /** * 初始化chromium */ - async browserInit () { + async browserInit() { if (this.browser) return this.browser if (this.lock) return false this.lock = true - logger.info('puppeteer Chromium 启动中...') + logger.info("puppeteer Chromium 启动中...") let connectFlag = false try { @@ -67,35 +62,32 @@ export default class Puppeteer extends Renderer { // 是否有browser实例 const browserUrl = (await redis.get(this.browserMacKey)) || this.config.wsEndpoint if (browserUrl) { - logger.info(`puppeteer Chromium from ${browserUrl}`) - const browserWSEndpoint = await puppeteer.connect({ browserWSEndpoint: browserUrl }).catch(() => { - logger.error('puppeteer Chromium 缓存的实例已关闭') - redis.del(this.browserMacKey) - }) - // 如果有实例,直接使用 - if (browserWSEndpoint) { - this.browser = browserWSEndpoint - if (this.browser) { + try { + const browserWSEndpoint = await puppeteer.connect({ browserWSEndpoint: browserUrl }) + // 如果有实例,直接使用 + if (browserWSEndpoint) { + this.browser = browserWSEndpoint connectFlag = true } + logger.info(`puppeteer Chromium 连接成功 ${browserUrl}`) + } catch (err) { + await redis.del(this.browserMacKey) } } - } catch (e) { - logger.info('puppeteer Chromium 不存在已有实例') - } + } catch (err) {} if (!this.browser || !connectFlag) { // 如果没有实例,初始化puppeteer this.browser = await puppeteer.launch(this.config).catch((err, trace) => { - let errMsg = err.toString() + (trace ? trace.toString() : '') - if (typeof err == 'object') { + let errMsg = err.toString() + (trace ? trace.toString() : "") + if (typeof err == "object") { logger.error(JSON.stringify(err)) } else { logger.error(err.toString()) - if (errMsg.includes('Could not find Chromium')) { - logger.error('没有正确安装 Chromium,可以尝试执行安装命令:node node_modules/puppeteer/install.js') - } else if (errMsg.includes('cannot open shared object file')) { - logger.error('没有正确安装 Chromium 运行库') + if (errMsg.includes("Could not find Chromium")) { + logger.error("没有正确安装 Chromium,可以尝试执行安装命令:node node_modules/puppeteer/install.js") + } else if (errMsg.includes("cannot open shared object file")) { + logger.error("没有正确安装 Chromium 运行库") } } logger.error(err, trace) @@ -105,33 +97,27 @@ export default class Puppeteer extends Renderer { this.lock = false if (!this.browser) { - logger.error('puppeteer Chromium 启动失败') + logger.error("puppeteer Chromium 启动失败") return false } - if (connectFlag) { - logger.info('puppeteer Chromium 已连接启动的实例') - } else { - logger.info(`[Chromium] ${this.browser.wsEndpoint()}`) - if (process.env.pm_id && this.browserMacKey) { + if (!connectFlag) { + logger.info(`puppeteer Chromium 启动成功 ${this.browser.wsEndpoint()}`) + if (this.browserMacKey) { // 缓存一下实例30天 const expireTime = 60 * 60 * 24 * 30 await redis.set(this.browserMacKey, this.browser.wsEndpoint(), { EX: expireTime }) } - logger.info('puppeteer Chromium 启动成功') } /** 监听Chromium实例是否断开 */ - this.browser.on('disconnected', () => { - logger.error('Chromium 实例关闭或崩溃!') - this.browser = false - }) + this.browser.on("disconnected", () => this.restart(true)) return this.browser } // 获取Mac地址 - getMac () { - let mac = '00:00:00:00:00:00' + getMac() { + let mac = "00:00:00:00:00:00" try { const network = os.networkInterfaces() let macFlag = false @@ -149,7 +135,7 @@ export default class Puppeteer extends Renderer { } } catch (e) { } - mac = mac.replace(/:/g, '') + mac = mac.replace(/:/g, "") return mac } @@ -168,18 +154,15 @@ export default class Puppeteer extends Renderer { * @param data.pageGotoParams 页面goto时的参数 * @return img 不做segment包裹 */ - async screenshot (name, data = {}) { - if (!await this.browserInit()) { + async screenshot(name, data = {}) { + if (!await this.browserInit()) return false - } const pageHeight = data.multiPageHeight || 4000 let savePath = this.dealTpl(name, data) - if (!savePath) { - return false - } + if (!savePath) return false - let buff = '' + let buff = "" let start = Date.now() let ret = [] @@ -187,17 +170,13 @@ export default class Puppeteer extends Renderer { const puppeteerTimeout = this.puppeteerTimeout let overtime - let overtimeFlag = false if (puppeteerTimeout > 0) { // TODO 截图超时处理 overtime = setTimeout(() => { - if (!overtimeFlag) { - logger.error(`[图片生成][${name}] 截图超时,当前等待队列:${this.shoting.join(',')}`) + if (this.shoting.length) { + logger.error(`[图片生成][${name}] 截图超时,当前等待队列:${this.shoting.join(",")}`) this.restart(true) this.shoting = [] - overtimeList.forEach(item => { - clearTimeout(item) - }) } }, puppeteerTimeout) } @@ -205,8 +184,8 @@ export default class Puppeteer extends Renderer { try { const page = await this.browser.newPage() let pageGotoParams = lodash.extend({ timeout: 120000 }, data.pageGotoParams || {}) - await page.goto(`file://${_path}${lodash.trim(savePath, '.')}`, pageGotoParams) - let body = await page.$('#container') || await page.$('body') + await page.goto(`file://${_path}${lodash.trim(savePath, ".")}`, pageGotoParams) + let body = await page.$("#container") || await page.$("body") // 计算页面高度 const boundingBox = await body.boundingBox() @@ -214,27 +193,27 @@ export default class Puppeteer extends Renderer { let num = 1 let randData = { - type: data.imgType || 'jpeg', + type: data.imgType || "jpeg", omitBackground: data.omitBackground || false, quality: data.quality || 90, - path: data.path || '' + path: data.path || "" } if (data.multiPage) { - randData.type = 'jpeg' + randData.type = "jpeg" num = Math.round(boundingBox.height / pageHeight) || 1 } - if (data.imgType === 'png') { + if (data.imgType === "png") { delete randData.quality } if (!data.multiPage) { buff = await body.screenshot(randData) + this.renderNum++ /** 计算图片大小 */ - const kb = (buff.length / 1024).toFixed(2) + 'KB' + const kb = (buff.length / 1024).toFixed(2) + "KB" logger.mark(`[图片生成][${name}][${this.renderNum}次] ${kb} ${logger.green(`${Date.now() - start}ms`)}`) - this.renderNum++ ret.push(buff) } else { // 分片截图 @@ -260,12 +239,12 @@ export default class Puppeteer extends Renderer { buff = await page.screenshot(randData) } if (num > 2) { - await Data.sleep(200) + await Bot.sleep(200) } this.renderNum++ /** 计算图片大小 */ - const kb = (buff.length / 1024).toFixed(2) + 'KB' + const kb = (buff.length / 1024).toFixed(2) + "KB" logger.mark(`[图片生成][${name}][${i}/${num}] ${kb}`) ret.push(buff) } @@ -273,22 +252,16 @@ export default class Puppeteer extends Renderer { logger.mark(`[图片生成][${name}] 处理完成`) } } - page.close().catch((err) => logger.error(err)) - } catch (error) { - logger.error(`[图片生成][${name}] 图片生成失败:${error}`) + page.close().catch(err => logger.error(err)) + } catch (err) { + logger.error(`[图片生成][${name}] 图片生成失败`, err) /** 关闭浏览器 */ - if (this.browser) { - await this.browser.close().catch((err) => logger.error(err)) - } - this.browser = false + this.restart(true) + if (overtime) clearTimeout(overtime) ret = [] return false } finally { - if (overtime) { - overtimeFlag = true - clearTimeout(overtime) - overtimeList = [] - } + if (overtime) clearTimeout(overtime) } this.shoting.pop() @@ -298,24 +271,26 @@ export default class Puppeteer extends Renderer { return false } - this.restart(false) - + this.restart() return data.multiPage ? ret : ret[0] } /** 重启 */ - restart (force = false) { + restart(force = false) { /** 截图超过重启数时,自动关闭重启浏览器,避免生成速度越来越慢 */ - if (this.renderNum % this.restartNum === 0 || force) { - if (this.shoting.length <= 0 || force) { - setTimeout(async () => { - if (this.browser) { - await this.browser.close().catch((err) => logger.error(err)) - } - this.browser = false - logger.info(`puppeteer Chromium ${force ? '强制' : ''}关闭重启...`) - }, 100) - } + if (!this.browser?.close || this.lock) return + if (!force) if (this.renderNum % this.restartNum !== 0 || this.shoting.length > 0) return + logger.info(`puppeteer Chromium ${force ? "强制" : ""}关闭重启...`) + this.stop(this.browser) + this.browser = false + return this.browserInit() + } + + async stop(browser) { + try { + await browser.close() + } catch (err) { + logger.error("puppeteer Chromium 关闭错误", err) } } -} +} \ No newline at end of file diff --git a/Yunzai/resources/http/File/404.jpg b/Yunzai/resources/http/File/404.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1ffae8c9e2eb1702a9700c2592c4cd3f6b6b83a2 Binary files /dev/null and b/Yunzai/resources/http/File/404.jpg differ diff --git a/Yunzai/resources/http/File/timeout.jpg b/Yunzai/resources/http/File/timeout.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c2a3dcb58f6b14817e76aaafbffbb3566903d1d Binary files /dev/null and b/Yunzai/resources/http/File/timeout.jpg differ