diff --git a/CHANGELOG.md b/CHANGELOG.md index e215f2d..d4d3335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 3.1.2 + +* 支持协议端:QQBot、OPQBot +* 新增`#绑定用户`命令 + * 可将其他QQ绑定至当前用户,以打通多个用户,子用户使用主用户的CK与UID等信息 + * 同时也可绑定其他平台的用户,例如频道、微信等。如需链接至其他平台需使用TRSS-Yunzai或Lain-plugin + * 部分命令可能无法识别绑定后的主用户,如遇问题可反馈 + # 3.1.1 * 支持协议端:米游社大别野Bot diff --git a/README.md b/README.md index f4be8ea..f397ff1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # TRSS-Yunzai -Yunzai 应用端,支持多账号,支持协议端:go-cqhttp、ComWeChat、GSUIDCore、ICQQ、QQ频道、微信、KOOK、Telegram、Discord +Yunzai 应用端,支持多账号,支持协议端:go-cqhttp、ComWeChat、GSUIDCore、ICQQ、QQBot、QQ频道、微信、KOOK、Telegram、Discord、OPQBot [![访问量](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) @@ -118,6 +118,12 @@ ws://localhost:2536/GSUIDCore +
QQBot + +[TRSS-Yunzai QQBot Plugin](../../../Yunzai-QQBot-Plugin) + +
+
QQ频道 [TRSS-Yunzai QQGuild Plugin](../../../Yunzai-QQGuild-Plugin) @@ -154,9 +160,19 @@ ws://localhost:2536/GSUIDCore
-
代理 +
OPQBot -[TRSS-Yunzai Proxy Plugin](../../../Yunzai-Proxy-Plugin) +下载运行 [OPQBot](https://opqbot.com),启动参数添加: + +``` +-wsserver ws://localhost:2536/OPQBot +``` + +
+ +
路由 + +[TRSS-Yunzai Route Plugin](../../../Yunzai-Route-Plugin)
diff --git a/lib/config/redis.js b/lib/config/redis.js index e4b84c0..aad2882 100644 --- a/lib/config/redis.js +++ b/lib/config/redis.js @@ -44,6 +44,7 @@ export default async function redisInit() { }) /** 全局变量 redis */ + client.url = redisUrl global.redis = client logger.info("Redis 连接成功") return client diff --git a/lib/plugins/loader.js b/lib/plugins/loader.js index fce9fa6..14b5364 100644 --- a/lib/plugins/loader.js +++ b/lib/plugins/loader.js @@ -134,7 +134,7 @@ class PluginsLoader { for (let val of files) { let filepath = "../../plugins/" + val.name let tmp = { - name: val.name, + name: val.name } if (val.isFile()) { if (!val.name.endsWith(".js")) continue @@ -229,8 +229,16 @@ class PluginsLoader { // 判断是否是星铁命令,若是星铁命令则标准化处理 // 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.isSr = true + e.game = "sr" e.msg = e.msg.replace(this.srReg, "#星铁") } diff --git a/lib/plugins/runtime.js b/lib/plugins/runtime.js index 21b2d46..35b4f23 100644 --- a/lib/plugins/runtime.js +++ b/lib/plugins/runtime.js @@ -77,7 +77,6 @@ export default class Runtime { await MysInfo.initCache() let runtime = new Runtime(e) e.runtime = runtime - e.game = e.isSr ? 'sr' : 'gs' await runtime.initUser() return runtime } @@ -88,7 +87,7 @@ export default class Runtime { if (user) { e.user = new Proxy(user, { get (self, key, receiver) { - let game = e.isSr ? 'sr' : 'gs' + let game = e.game let fnMap = { uid: 'getUid', uidList: 'getUidList', diff --git a/lib/tools/name.js b/lib/tools/log.js similarity index 95% rename from lib/tools/name.js rename to lib/tools/log.js index d4ff2f2..33799aa 100644 --- a/lib/tools/name.js +++ b/lib/tools/log.js @@ -13,7 +13,6 @@ fs.readFile(`${_path}/config/pm2/pm2.json`, `utf8`, (err, data) => { const config = JSON.parse(data) if (config.apps && config.apps.length > 0 && config.apps[0].name) { const appName = config.apps[0].name - console.log(config.apps[0].name) runPm2Logs(appName) } else { console.log('读取失败:无法在pm2.json中找到name数组') @@ -32,4 +31,4 @@ function runPm2Logs(appName) { console.error(`pm2 logs process exited with code ${code}`) } }) -} \ No newline at end of file +} diff --git a/package.json b/package.json index d5ce248..f10c4a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trss-yunzai", - "version": "3.1.0", + "version": "3.1.2", "author": "TimeRainStarSky, Yoimiya-Kokomi, Le-niao", "description": "Bot", "main": "app.js", @@ -13,14 +13,14 @@ "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/name.js" + "log": "node ./lib/tools/log.js" }, "dependencies": { "art-template": "^4.13.2", "chalk": "^5.3.0", "chokidar": "^3.5.3", "express": "^4.18.2", - "file-type": "^18.5.0", + "file-type": "^18.6.0", "https-proxy-agent": "7.0.2", "image-size": "^1.0.2", "lodash": "^4.17.21", @@ -32,7 +32,7 @@ "node-xlsx": "^0.23.0", "oicq": "link:lib/modules/oicq", "pm2": "^5.3.0", - "puppeteer": "^21.3.8", + "puppeteer": "^21.4.1", "redis": "^4.6.10", "sequelize": "^6.33.0", "sqlite3": "^5.1.6", @@ -40,9 +40,9 @@ "yaml": "^2.3.3" }, "devDependencies": { - "eslint": "^8.51.0", + "eslint": "^8.52.0", "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.28.1", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-n": "^16.2.0", "eslint-plugin-promise": "^6.1.1" }, diff --git a/plugins/adapter/ComWeChat.js b/plugins/adapter/ComWeChat.js index a0b910c..cef7d28 100644 --- a/plugins/adapter/ComWeChat.js +++ b/plugins/adapter/ComWeChat.js @@ -31,8 +31,7 @@ Bot.adapter.push(new class ComWeChatAdapter { sendApi(ws, action, params = {}) { const echo = randomUUID() - const msg = { action, params, echo } - ws.sendMsg(msg) + ws.sendMsg({ action, params, echo }) return new Promise(resolve => Bot.once(echo, data => resolve({ ...data, ...data.data }))) diff --git a/plugins/adapter/OPQBot.js b/plugins/adapter/OPQBot.js new file mode 100644 index 0000000..80aabed --- /dev/null +++ b/plugins/adapter/OPQBot.js @@ -0,0 +1,338 @@ +import path from "node:path" +import fs from "node:fs" + +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))) + } + + 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:\/\/.*?"/g, 'base64://..."') + } + + async uploadFile(id, type, file) { + const opts = { CommandId: this.CommandId[type] } + + if (file.match(/^base64:\/\//)) + opts.Base64Buf = file.replace(/^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": + 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) { + logger.info(`${logger.blue(`[${data.self_id}]`)} 发送好友消息:[${data.user_id}] ${this.makeLog(msg)}`) + 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) { + logger.info(`${logger.blue(`[${data.self_id}]`)} 发送群员消息:[${data.group_id}, ${data.user_id}] ${this.makeLog(msg)}`) + 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) { + logger.info(`${logger.blue(`[${data.self_id}]`)} 发送群消息:[${data.group_id}] ${this.makeLog(msg)}`) + 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), + } + } + + 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), + } + } + + 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" + + logger.info(`${logger.blue(`[${data.self_id}]`)} 好友消息:[${data.sender.nickname}(${data.user_id})] ${data.raw_message}`) + 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) + logger.info(`${logger.blue(`[${data.self_id}]`)} 群消息:[${data.group_name}(${data.group_id}), ${data.sender.nickname}(${data.user_id})] ${data.raw_message}`) + 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: + logger.warn(`${logger.blue(`[${id}]`)} 未知事件:${logger.magenta(JSON.stringify(data))}`) + } + } + + 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, + } + + logger.mark(`${logger.blue(`[${id}]`)} ${this.name}(${this.id}) ${this.version} 已连接`) + Bot.em(`connect.${id}`, { self_id: id }) + } + + message(data, ws) { + try { + data = JSON.parse(data) + } catch (err) { + return logger.error(`解码数据失败:${logger.red(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 { + logger.warn(`${logger.blue(`[${id}]`)} 未知消息:${logger.magenta(JSON.stringify(data))}`) + } + } + + 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/plugins/adapter/go-cqhttp.js b/plugins/adapter/go-cqhttp.js index 78cc783..343f4d8 100644 --- a/plugins/adapter/go-cqhttp.js +++ b/plugins/adapter/go-cqhttp.js @@ -30,8 +30,7 @@ Bot.adapter.push(new class gocqhttpAdapter { sendApi(ws, action, params) { const echo = randomUUID() - const msg = { action, params, echo } - ws.sendMsg(msg) + ws.sendMsg({ action, params, echo }) return new Promise(resolve => Bot.once(echo, data => resolve({ ...data, ...data.data }))) diff --git a/plugins/other/update.js b/plugins/other/update.js index a33c30f..405c078 100644 --- a/plugins/other/update.js +++ b/plugins/other/update.js @@ -11,7 +11,7 @@ const { exec, execSync } = require('child_process') let uping = false export class update extends plugin { - constructor() { + constructor () { super({ name: '更新', dsc: '#更新 #强制更新', @@ -37,18 +37,28 @@ export class update extends plugin { this.typeName = 'TRSS-Yunzai' } - async update() { + async update () { if (!this.e.isMaster) return false if (uping) return this.reply('已有命令更新中..请勿重复操作') if (/详细|详情|面板|面版/.test(this.e.msg)) return false /** 获取插件 */ - const plugin = this.getPlugin() + let plugin = this.getPlugin() if (plugin === false) return false /** 执行更新 */ - await this.runUpdate(plugin) + if (plugin === '') { + await this.runUpdate('') + await common.sleep(1000) + plugin = this.getPlugin('genshin') + await this.runUpdate(plugin) + await common.sleep(1000) + plugin = this.getPlugin('miao-plugin') + await this.runUpdate(plugin) + } else { + await this.runUpdate(plugin) + } /** 是否需要重启 */ if (this.isUp) { @@ -57,7 +67,7 @@ export class update extends plugin { } } - getPlugin(plugin = '') { + getPlugin (plugin = '') { if (!plugin) { plugin = this.e.msg.replace(/#(强制)?更新(日志)?/, '') if (!plugin) return '' @@ -69,7 +79,7 @@ export class update extends plugin { return plugin } - async execSync(cmd) { + async execSync (cmd) { return new Promise((resolve, reject) => { exec(cmd, { windowsHide: true }, (error, stdout, stderr) => { resolve({ error, stdout, stderr }) @@ -77,7 +87,7 @@ export class update extends plugin { }) } - async runUpdate(plugin = '') { + async runUpdate (plugin = '') { this.isNowUp = false let cm = 'git pull --no-rebase' @@ -118,7 +128,7 @@ export class update extends plugin { return true } - async getcommitId(plugin = '') { + async getcommitId (plugin = '') { let cm = 'git rev-parse --short HEAD' if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` @@ -126,7 +136,7 @@ export class update extends plugin { return lodash.trim(commitId) } - async getTime(plugin = '') { + async getTime (plugin = '') { let cm = 'git log -1 --pretty=%cd --date=format:"%F %T"' if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` @@ -142,7 +152,7 @@ export class update extends plugin { return time } - async gitErr(err, stdout) { + async gitErr (err, stdout) { const msg = '更新失败!' const errMsg = err.toString() stdout = stdout.toString() @@ -168,7 +178,7 @@ export class update extends plugin { return this.reply([errMsg, stdout]) } - async updateAll() { + async updateAll () { const dirs = fs.readdirSync('./plugins/') await this.runUpdate() @@ -186,11 +196,11 @@ export class update extends plugin { } } - restart() { + restart () { new Restart(this.e).restart() } - async getLog(plugin = '') { + async getLog (plugin = '') { let cm = 'git log -100 --pretty="%h||[%cd] %s" --date=format:"%F %T"' if (plugin) cm = `cd "plugins/${plugin}" && ${cm}` @@ -232,9 +242,9 @@ export class update extends plugin { return common.makeForwardMsg(this.e, [log, end], `${plugin || 'TRSS-Yunzai'} 更新日志,共${line}条`) } - async updateLog() { + async updateLog () { const plugin = this.getPlugin() if (plugin === false) return false return this.reply(await this.getLog(plugin)) } -} \ No newline at end of file +}