dfghfhgfg's picture
Upload 75 files
3b148c3 verified
import util from "node:util"
import fs from "node:fs/promises"
import lodash from "lodash"
import cfg from "../config/config.js"
import plugin from "./plugin.js"
import schedule from "node-schedule"
import { segment } from "oicq"
import chokidar from "chokidar"
import moment from "moment"
import path from "node:path"
import Runtime from "./runtime.js"
import Handler from "./handler.js"
/** 全局变量 plugin */
global.plugin = plugin
global.segment = segment
/**
* 加载插件
*/
class PluginsLoader {
priority = []
handler = {}
task = []
dir = "plugins"
/** 命令冷却cd */
groupCD = {}
singleCD = {}
/** 插件监听 */
watcher = {}
eventMap = {
message: ["post_type", "message_type", "sub_type"],
notice: ["post_type", "notice_type", "sub_type"],
request: ["post_type", "request_type", "sub_type"],
}
msgThrottle = {}
/** 星铁命令前缀 */
srReg = /^#?(\*|星铁|星轨|穹轨|星穹|崩铁|星穹铁道|崩坏星穹铁道|铁道)+/
/** 绝区零前缀 */
zzzReg = /^#?(%|%|绝区零|绝区)+/
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) {
if (isRefresh) this.priority = []
if (this.priority.length) return
Bot.makeLog("info", "-----------", "Plugin")
Bot.makeLog("info", "加载插件中...", "Plugin")
const files = await this.getPlugins()
this.pluginCount = 0
const packageErr = []
await Promise.allSettled(files.map(file =>
this.importPlugin(file, packageErr)
))
this.packageTips(packageErr)
this.createTask()
Bot.makeLog("info", `加载定时任务[${this.task.length}个]`, "Plugin")
Bot.makeLog("info", `加载插件[${this.pluginCount}个]`, "Plugin")
/** 优先级排序 */
this.priority = lodash.orderBy(this.priority, ["priority"], ["asc"])
}
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") {
Bot.makeLog("error", [`插件加载错误 ${logger.red(file.name)}`, i], "Plugin")
}
} catch (error) {
if (packageErr && error.stack.includes("Cannot find package")) {
packageErr.push({ error, file })
} else {
Bot.makeLog("error", [`插件加载错误 ${logger.red(file.name)}`, error], "Plugin")
}
}
}
async loadPlugin(file, p) {
if (!p?.prototype) return
this.pluginCount++
const plugin = new p
Bot.makeLog("debug", `加载插件 [${file.name}][${plugin.name}]`, "Plugin")
/** 执行初始化,返回 return 则跳过加载 */
if (plugin.init && await plugin.init() === "return") return
/** 初始化定时任务 */
this.collectTask(plugin.task, plugin.name)
/** 初始化正则表达式 */
if (plugin.rule) for (const i of plugin.rule)
if (!(i.reg instanceof RegExp))
i.reg = new RegExp(i.reg)
this.priority.push({
plugin,
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,
self: plugin,
property: priority || plugin.priority || 500,
fn: plugin[fn],
})
})
}
}
packageTips(packageErr) {
if (!packageErr.length) return
Bot.makeLog("error", "--------- 插件加载错误 ---------", "Plugin")
for (const i of packageErr) {
const pack = i.error.stack.match(/'(.+?)'/g)[0].replace(/'/g, "")
Bot.makeLog("error", `${logger.cyan(i.file.name)} 缺少依赖 ${logger.red(pack)}`, "Plugin")
}
Bot.makeLog("error", `安装插件后请 ${logger.red("pnpm i")} 安装依赖`, "Plugin")
Bot.makeLog("error", `仍报错${logger.red("进入插件目录")} pnpm add 依赖`, "Plugin")
Bot.makeLog("error", "--------------------------------", "Plugin")
}
/**
* 处理事件
*
* 参数文档 https://github.com/TimeRainStarSky/Yunzai/tree/docs
* @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.reply(e)
/** 注册runtime */
await Runtime.init(e)
const priority = []
for (const i of this.priority) {
/** 判断是否启用功能,过滤事件 */
if (this.checkDisable(Object.assign(i.plugin, { e })) && this.filtEvent(e, i.plugin))
priority.push(i)
}
for (const i of priority) {
/** 上下文hook */
if (!i.plugin.getContext) continue
const context = {
...i.plugin.getContext(),
...i.plugin.getContext(false, true),
}
if (!lodash.isEmpty(context)) {
let ret
for (const fnc in context)
ret ||= await Object.assign(new i.class(e), { e })[fnc](context[fnc])
if (ret === "continue") continue
return
}
}
/** 是否只关注主动at */
if (!this.onlyReplyAt(e)) return
// 判断是否是星铁命令,若是星铁命令则标准化处理
// e.isSr = true,且命令标准化为 #星铁 开头
Object.defineProperty(e, "isSr", {
get: () => e.game === "sr",
set: (v) => e.game = v ? "sr" : "gs"
})
Object.defineProperty(e, "isGs", {
get: () => e.game === "gs",
set: (v) => e.game = v ? "gs" : "sr"
})
if (this.srReg.test(e.msg)) {
e.game = "sr"
e.msg = e.msg.replace(this.srReg, "#星铁")
} else if (this.zzzReg.test(e.msg)) {
e.game = "zzz"
e.msg = e.msg.replace(this.zzzReg, "#绝区零")
}
/** 优先执行 accept */
for (const i of priority)
if (i.plugin.accept) {
const res = await Object.assign(new i.class(e), { e }).accept(e)
if (res === "return") return
if (res) break
}
a: for (const i of priority) {
if (i.plugin.rule) for (const v of i.plugin.rule) {
/** 判断事件 */
if (v.event && !this.filtEvent(e, v)) continue
/** 正则匹配 */
if (!v.reg.test(e.msg)) continue
const plugin = Object.assign(new i.class(e), { e })
e.logFnc = `${logger.blue(`[${plugin.name}(${v.fnc})]`)}`
Bot.makeLog(v.log === false ? "debug" : "info", `${e.logText}${e.logFnc}${logger.yellow("[开始处理]")}`, false)
/** 判断权限 */
if (!this.filtPermission(e, v)) break a
try {
const start_time = Date.now()
const res = plugin[v.fnc] && (await plugin[v.fnc](e))
if (res !== false) {
/** 设置冷却cd */
this.setLimit(e)
Bot.makeLog(v.log === false ? "debug" : "mark", `${e.logText}${e.logFnc}${logger.green(`[完成${Bot.getTimeDiff(start_time)}]`)}`, false)
break a
}
} catch (err) {
Bot.makeLog("error", [`${e.logText}${e.logFnc}`, err], false)
break a
}
}
}
}
/** 过滤事件 */
filtEvent(e, v) {
if (!v.event) return false
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]])
}
return v.event === newEvent.join(".")
}
/** 判断权限 */
filtPermission(e, v) {
if (v.permission === "all" || !v.permission) return true
if (v.permission === "master") {
if (e.isMaster) {
return true
} else {
e.reply("暂无权限,只有主人才能操作")
return false
}
}
if (e.isGroup) {
if (v.permission === "owner") {
if (!e.member.is_owner) {
e.reply("暂无权限,只有群主才能操作")
return false
}
}
if (v.permission === "admin") {
if (!e.member.is_admin) {
e.reply("暂无权限,只有管理员才能操作")
return false
}
}
}
return true
}
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机器人
* @param e.at 是否at,多个at 以最后的为准
* @param e.file 接受到的文件
* @param e.isPrivate 是否私聊
* @param e.isGroup 是否群聊
* @param e.isMaster 是否管理员
* @param e.logText 日志用户字符串
* @param e.logFnc 日志方法字符串
*/
dealEvent(e) {
if (e.message) for (const i of e.message) {
switch (i.type) {
case "text":
e.msg = (e.msg || "") + this.dealText(i.text)
break
case "image":
if (Array.isArray(e.img))
e.img.push(i.url)
else
e.img = [i.url]
break
case "at":
if (i.qq == e.self_id)
e.atBot = true
else
e.at = i.qq
break
case "reply":
e.reply_id = i.id
if (e.group?.getMsg)
e.getReply = () => e.group.getMsg(e.reply_id)
else if (e.friend?.getMsg)
e.getReply = () => e.friend.getMsg(e.reply_id)
break
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
}
}
e.logText = ""
if (e.message_type === "private" || e.notice_type === "friend") {
e.isPrivate = true
if (e.sender) {
e.sender.card = e.sender.nickname
} else {
e.sender = {
user_id: e.user_id,
nickname: e.friend?.nickname,
card: e.friend?.nickname,
}
}
e.logText = `[${e.sender?.nickname ? `${e.sender.nickname}(${e.user_id})` : e.user_id}]`
} else if (e.message_type === "group" || e.notice_type === "group") {
e.isGroup = true
if (e.sender) {
if (!e.sender.card)
e.sender.card = e.sender.nickname
} else {
e.sender = {
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 = e.group.name
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}]`
}
e.logText = `${logger.cyan(e.logText)}${logger.red(`[${lodash.truncate(e.msg || e.raw_message || Bot.String(e), { length: 100 })}]`)}`
if (e.user_id && cfg.master[e.self_id]?.includes(String(e.user_id)))
e.isMaster = true
/** 只关注主动at msg处理 */
if (e.msg && e.isGroup) {
const alias = cfg.getGroup(e.self_id, e.group_id).botAlias
for (const i of Array.isArray(alias) ? alias : [alias])
if (e.msg.startsWith(i)) {
e.msg = lodash.trimStart(e.msg, i).trim()
e.hasAlias = true
break
}
}
}
/** 处理回复,捕获发送失败异常 */
reply(e) {
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 发送的消息
* @param quote 是否引用回复
* @param data.recallMsg 是否撤回消息,0-120秒,0不撤回
* @param data.at 是否提及用户
*/
e.reply = async (msg = "", quote = false, data = {}) => {
if (!msg) return false
let { recallMsg = 0, at = "" } = data
if (at && e.isGroup) {
if (at === true)
at = e.user_id
if (Array.isArray(msg))
msg.unshift(segment.at(at), "\n")
else
msg = [segment.at(at), "\n", msg]
}
if (quote && e.message_id) {
if (Array.isArray(msg))
msg.unshift(segment.reply(e.message_id))
else
msg = [segment.reply(e.message_id), msg]
}
let res
try {
res = await reply(msg)
} catch (err) {
Bot.makeLog("error", ["发送消息错误", msg, err], e.self_id)
res = { error: [err] }
}
if (recallMsg > 0 && res?.message_id) {
if (e.group?.recallMsg)
setTimeout(() => {
e.group.recallMsg(res.message_id)
if (e.message_id)
e.group.recallMsg(e.message_id)
}, recallMsg * 1000)
else if (e.friend?.recallMsg)
setTimeout(() => {
e.friend.recallMsg(res.message_id)
if (e.message_id)
e.friend.recallMsg(e.message_id)
}, recallMsg * 1000)
}
this.count(e, "send", msg)
return res
}
}
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`)
}
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}`)
}
for (const i of key)
await redis.incr(`Yz:count:${type}:${i}`)
}
/** 收集定时任务 */
collectTask(task, name) {
for (const i of Array.isArray(task) ? task : [task])
if (i.cron && i.fnc) {
i.name ??= name
this.task.push(i)
}
}
/** 创建定时任务 */
createTask() {
const created = []
for (const i of this.task) {
if (i.job?.cancel) i.job.cancel()
const name = `${logger.blue(`[${i.name}(${i.cron})]`)}`
if (created.includes(name)) {
Bot.makeLog("warn", `重复定时任务 ${name} 已跳过`, "Task")
continue
}
created.push(name)
Bot.makeLog("debug", `加载定时任务 ${name}`, "Task")
i.job = schedule.scheduleJob(i.cron, async () => { try {
const start_time = Date.now()
Bot.makeLog(i.log === false ? "debug" : "mark", `${name}${logger.yellow("[开始处理]")}`, false)
await i.fnc()
Bot.makeLog(i.log === false ? "debug" : "mark", `${name}${logger.green(`[完成${Bot.getTimeDiff(start_time)}]`)}`, false)
} catch (err) {
Bot.makeLog("error", [name, err], false)
}})
}
}
/** 检查命令冷却cd */
checkLimit(e) {
/** 禁言中 */
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)
if (config.groupCD && this.groupCD[e.group_id])
return false
if (config.singleCD && this.singleCD[`${e.group_id}.${e.user_id}`])
return false
const msgId = `${e.self_id}:${e.user_id}:${e.raw_message}`
if (this.msgThrottle[msgId]) return false
this.msgThrottle[msgId] = true
setTimeout(() => delete this.msgThrottle[msgId], 1000)
return true
}
/** 设置冷却cd */
setLimit(e) {
if (!e.message || e.isPrivate) return
let config = cfg.getGroup(e.self_id, e.group_id)
if (config.groupCD) {
this.groupCD[e.group_id] = true
setTimeout(() => delete this.groupCD[e.group_id], config.groupCD)
}
if (config.singleCD) {
const key = `${e.group_id}.${e.user_id}`
this.singleCD[key] = true
setTimeout(() => delete this.singleCD[key], config.singleCD)
}
}
/** 是否只关注主动at */
onlyReplyAt(e) {
if (!e.message || e.isPrivate) return true
let groupCfg = cfg.getGroup(e.self_id, e.group_id)
/** 模式0,未开启前缀 */
if (groupCfg.onlyReplyAt === 0 || !groupCfg.botAlias) return true
/** 模式2,非主人开启 */
if (groupCfg.onlyReplyAt === 2 && e.isMaster) return true
/** at机器人 */
if (e.atBot) return true
/** 消息带前缀 */
if (e.hasAlias) return true
return false
}
/** 判断黑白名单 */
checkBlack(e) {
const other = cfg.getOther()
/** 黑名单用户 */
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.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(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 (groupCfg.enable?.length && !groupCfg.enable.includes(p.name))
return false
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
if (plugin.rule) for (const i of plugin.rule)
if (!(i.reg instanceof RegExp))
i.reg = new RegExp(i.reg)
for (const i of this.priority)
if (i.key === key && i.name === plugin.name)
Object.assign(i, {
plugin,
class: p,
priority: plugin.priority,
})
})
this.priority = lodash.orderBy(this.priority, ["priority"], ["asc"])
} catch (err) {
Bot.makeLog("error", [`插件加载错误 ${logger.red(key)}`, err], "Plugin")
}
}
/** 监听热更新 */
watch(dirName, appName) {
this.watchDir(dirName)
if (this.watcher[`${dirName}.${appName}`]) return
const file = `./${this.dir}/${dirName}/${appName}`
const watcher = chokidar.watch(file)
const key = `${dirName}/${appName}`
/** 监听修改 */
watcher.on("change", path => {
Bot.makeLog("mark", `[修改插件][${dirName}][${appName}]`, "Plugin")
this.changePlugin(key)
})
/** 监听删除 */
watcher.on("unlink", async path => {
Bot.makeLog("mark", `[卸载插件][${dirName}][${appName}]`, "Plugin")
/** 停止更新监听 */
this.watcher[`${dirName}.${appName}`].removeAllListeners("change")
this.priority = this.priority.filter(i => i.key !== key)
})
this.watcher[`${dirName}.${appName}`] = watcher
}
/** 监听文件夹更新 */
watchDir(dirName) {
if (this.watcher[dirName]) return
const watcher = chokidar.watch(`./${this.dir}/${dirName}/`)
/** 热更新 */
Bot.once("online", () => {
/** 新增文件 */
watcher.on("add", async PluPath => {
const appName = path.basename(PluPath)
if (!appName.endsWith(".js")) return
Bot.makeLog("mark", `[新增插件][${dirName}][${appName}]`, "Plugin")
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)
})
})
this.watcher[dirName] = watcher
}
}
export default new PluginsLoader()