UNPKG

onebots

Version:

基于icqq的多例oneBot实现

690 lines (689 loc) 27.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.V11 = void 0; const action_1 = require("./action"); const onebot_1 = require("../../onebot"); const crypto_1 = __importDefault(require("crypto")); const ws_1 = require("ws"); const url_1 = require("url"); const utils_1 = require("../../utils"); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const path_1 = require("path"); const app_1 = require("../../server/app"); const service_1 = require("../../service"); const db_1 = require("../../db"); const sendMsgTypes = ["private", "group", "discuss"]; class V11 extends service_1.Service { constructor(oneBot, config) { super(oneBot.adapter, config); this.oneBot = oneBot; this.config = config; this.version = "V11"; this.timestamp = Date.now(); this._queue = []; this.queue_running = false; this.wsr = new Set(); this.action = new action_1.Action(); this.logger = this.oneBot.adapter.getLogger(this.oneBot.uin, this.version); this.db = new db_1.JsonDB((0, path_1.join)(app_1.App.configDir, "data", `${this.oneBot.uin}_v11.jsondb`)); this.oneBot.on("online", async () => { this.logger.info("【好友列表】"); const friendList = await this.oneBot.getFriendList("V11"); friendList.forEach(item => this.logger.info(`\t${item.user_name}(${item.user_id})`)); this.logger.info("【群列表】"); const groupList = await this.oneBot.getGroupList("V11"); groupList.forEach(item => this.logger.info(`\t${item.group_name}(${item.group_id})`)); this.logger.info(""); }); } transformToInt(path, value) { if (!value || typeof value !== "string") throw new Error(`value must be string`); value = value.replace(/\./g, "%46"); const obj = this.db.get(path, {}); if (obj[value]) return obj[value]; const int = (0, utils_1.randomInt)(1000, Number.MAX_SAFE_INTEGER); const isExist = () => { return Object.keys(obj).some(key => { return obj[key] === int; }); }; // 虽然重复概率小,但还是避免下 if (isExist()) return this.transformToInt(path, value); this.db.set(`${path}.${value}`, int); const keys = Object.keys(obj); if (keys.length > 1000) this.db.delete(`${path}.${keys.shift()}`); return int; } transformStrToIntForObj(obj, keys) { if (!obj) return; for (const key of keys) { const value = obj[key]; if (typeof value !== "string") continue; Reflect.set(obj, key, this.transformToInt(key, value)); } } getStrByInt(path, value) { const obj = this.db.get(path, {}); return (Object.keys(obj) .find(str => { return obj[str] == value; }) ?.replace(/%46/g, ".") || value + ""); } start() { if (this.config.use_http) this.startHttp(); if (this.config.use_ws) this.startWs(); this.config.http_reverse.forEach(config => { if (typeof config === "string") { config = { url: config, access_token: this.config.access_token, secret: this.config.secret, }; } else { config = { access_token: this.config.access_token, secret: this.config.secret, ...config, }; } this.startHttpReverse(config); }); this.config.ws_reverse.forEach(config => { this.startWsReverse(config); }); this.on("dispatch", serialized => { for (const ws of this.wss?.clients || []) { ws.send(serialized, err => { if (err) this.logger.error(`正向WS(${ws.url})上报事件失败: ` + err.message); else this.logger.debug(`正向WS(${ws.url})上报事件成功: ` + serialized); }); } for (const ws of this.wsr) { ws.send(serialized, err => { if (err) { this.logger.error(`反向WS(${ws.url})上报事件失败: ` + err.message); } else this.logger.debug(`反向WS(${ws.url})上报事件成功: ` + serialized); }); } }); if (this.config.heartbeat) { this.heartbeat = setInterval(() => { this.dispatch({ self_id: this.oneBot.uin, status: { online: this.oneBot.status === onebot_1.OneBotStatus.Online, good: this.oneBot.app.isStarted, }, time: Math.floor(Date.now() / 1000), post_type: "meta_event", meta_event_type: "heartbeat", interval: this.config.heartbeat * 1000, }); }, this.config.heartbeat * 1000); } this.oneBot.on("message.receive", event => { const payload = this.adapter.formatEventPayload(this.oneBot.uin, "V11", "message", event); this.dispatch(payload); }); this.oneBot.on("notice.receive", event => { const payload = this.adapter.formatEventPayload(this.oneBot.uin, "V11", "notice", event); this.dispatch(payload); }); this.oneBot.on("request.receive", event => { const payload = this.adapter.formatEventPayload(this.oneBot.uin, "V11", "request", event); this.dispatch(payload); }); } startHttp() { this.oneBot.app.router.all(new RegExp(`^${this.path}/(.*)$`), this._httpRequestHandler.bind(this)); this.logger.mark(`开启http服务器成功,监听:http://127.0.0.1:${this.oneBot.app.config.port}${this.path}`); } startHttpReverse(config) { this.on("dispatch", (serialized) => { const options = { method: "POST", timeout: this.config.post_timeout * 1000, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(serialized), "X-Self-ID": String(this.oneBot.uin), "User-Agent": "OneBot", }, }; if (this.config.secret) { //@ts-ignore options.headers["X-Signature"] = "sha1=" + crypto_1.default .createHmac("sha1", String(this.config.secret)) .update(serialized) .digest("hex"); } const protocol = config.url.startsWith("https") ? https_1.default : http_1.default; try { protocol .request(config.url, options, res => { if (res.statusCode !== 200) return this.logger.warn(`POST(${config.url})上报事件收到非200响应:` + res.statusCode); let data = ""; res.setEncoding("utf-8"); res.on("data", chunk => (data += chunk)); res.on("end", () => { this.logger.debug(`收到HTTP响应 ${res.statusCode} :` + data); if (!data) return; try { this._quickOperate(JSON.parse(serialized), JSON.parse(data)); } catch (e) { this.logger.error(`快速操作遇到错误:` + e.message); } }); }) .on("error", err => { this.logger.error(`POST(${config.url})上报事件失败:` + err.message); }) .end(serialized, () => { this.logger.debug(`POST(${config.url})上报事件成功: ` + serialized); }); } catch (e) { this.logger.error(`POST(${config.url})上报失败:` + e.message); } }); } startWs() { this.wss = this.oneBot.app.router.ws(this.path); this.logger.mark(`开启ws服务器成功,监听:ws://127.0.0.1:${this.oneBot.app.config.port}${this.path}`); this.wss.on("error", err => { this.logger.error(err.message); }); this.wss.on("connection", (ws, req) => { this.logger.info(`ws客户端(${req.url})已连接`); ws.on("error", err => { this.logger.error(`ws客户端(${req.url})报错:${err.message}`); }); ws.on("close", (code, reason) => { this.logger.warn(`ws客户端(${req.url})连接关闭,关闭码${code},关闭理由:` + reason); }); if (this.config.access_token) { const url = new url_1.URL(req.url, "http://127.0.0.1"); const token = url.searchParams.get("access_token"); if (token) req.headers["authorization"] = `Bearer ${token}`; if (!req.headers["authorization"] || req.headers["authorization"] !== `Bearer ${this.config.access_token}`) return ws.close(1002, "wrong access token"); } this._webSocketHandler(ws); }); } startWsReverse(url) { this._createWsr(url); } async stop(force) { for (const ws of this.wss.clients) { ws.close(); } this.wss.close(); for (const ws of this.wsr) { ws.close(); } } format(_, data) { return data; } async dispatch(data) { if (!this.filterFn(data)) return; data.post_type = data.post_type || "system"; if (data.message && data.post_type === "message") { data.message = this.adapter.transformMessage(this.oneBot.uin, "V11", data.message); } data.time = Math.floor(Date.now() / 1000); // data = transformObj(data, (key, value) => { // if (!['user_id', 'group_id', 'discuss_id', 'member_id', 'channel_id', 'guild_id'].includes(key)) return value // return value + '' // }) if (!this.filterFn(data)) return; this.emit("dispatch", this._formatEvent(data)); } _formatEvent(data) { if (data.post_type === "notice") { const data1 = { ...data }; if (data.notice_type === "group") { delete data1.group; delete data1.member; switch (data.sub_type) { case "decrease": data1.sub_type = data.operator_id === data.user_id ? "leave" : data.user_id === this.oneBot.uin ? "kick_me" : "kick"; data1.notice_type = `${data.notice_type}_${data.sub_type}`; break; case "increase": data1.notice_type = `${data.notice_type}_${data.sub_type}`; data1.sub_type = "approve"; // todo 尚未实现 data1.operator_id = data1.user_id; // todo 尚未实现 break; case "ban": data1.notice_type = `${data.notice_type}_${data.sub_type}`; data1.subtype = data.duration ? "ban" : "lift_ban"; break; case "recall": data1.notice_type = `${data.notice_type}_${data.sub_type}`; delete data1.sub_type; break; case "admin": data1.notice_type = `${data.notice_type}_${data.sub_type}`; data1.sub_type = data.set ? "set" : "unset"; break; case "poke": data1.notice_type = "notify"; data1.user_id = data.operator_id; break; default: break; } } else { delete data1.friend; switch (data.sub_type) { case "increase": data1.notice_type = `friend_add`; break; case "recall": data1.notice_type = `friend_recall`; break; default: break; } } return JSON.stringify(data1, (_, v) => (typeof v === "bigint" ? v.toString() : v)); } else { delete data.bot; return JSON.stringify(data, (_, v) => (typeof v === "bigint" ? v.toString() : v)); } } async getReplyMsgIdFromDB(data) { let group_id = data.message_type === "group" ? data.group_id : 0; let msg = await this.db.find("messages", message => { return (message.user_id === data.user_id && message.group_id === group_id && message.seq === data.source.seq); }); return msg?.id || 0; } async _httpRequestHandler(ctx) { if (ctx.method === "OPTIONS") { return ctx .writeHead(200, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, authorization", }) .end(); } const url = new url_1.URL(ctx.url, `http://127.0.0.1`); if (this.config.access_token) { if (ctx.headers["authorization"]) { if (ctx.headers["authorization"] !== `Bearer ${this.config.access_token}`) return ctx.res.writeHead(403).end(); } else { const access_token = url.searchParams.get("access_token"); if (!access_token) return ctx.res.writeHead(401).end(); else if (access_token !== this.config.access_token) return ctx.res.writeHead(403).end(); } } ctx.res.setHeader("Content-Type", "application/json; charset=utf-8"); if (this.config.enable_cors) ctx.res.setHeader("Access-Control-Allow-Origin", "*"); const action = url.pathname.replace(`${this.path}`, "").slice(1); if (ctx.method === "GET") { try { const ret = await this.apply({ action, params: ctx.query }); ctx.res.writeHead(200).end(ret); } catch (e) { ctx.res.writeHead(500).end(e.message); } } else if (ctx.method === "POST") { try { const params = { ...(ctx.request.query || {}), ...(ctx.request.body || {}), }; const ret = await this.apply({ action, params }); ctx.res.writeHead(200).end(ret); } catch (e) { ctx.res.writeHead(500).end(e.message); } } else { ctx.res.writeHead(405).end(); } } /** * 处理ws消息 */ _webSocketHandler(ws) { ws.on("message", async (msg) => { const msgStr = msg.toString(); this.logger.info(" 收到ws消息:", msgStr.length > 2e3 ? msgStr.slice(0, 2e3) + ` ... ${msgStr.length - 2e3} more chars` : msgStr); var data; try { data = JSON.parse(msgStr); let ret; if (data.action?.startsWith(".handle_quick_operation")) { const event = data.params.context, res = data.params.operation; this._quickOperate(event, res); ret = JSON.stringify({ retcode: 0, status: "async", data: null, error: null, echo: data.echo, }); } else { ret = await this.apply(data); } ws.send(ret); } catch (e) { let code, message; if (e instanceof onebot_1.NotFoundError) { code = 1404; message = "不支持的api"; } else { code = 1400; message = "请求格式错误"; } ws.send(JSON.stringify({ retcode: code, status: "failed", data: null, error: { code, message, }, echo: data?.echo, msg: e.message, // gocq 返回的消息里有这个字段且很多插件都在访问 action: data.action, })); } }); ws.send(JSON.stringify(V11.genMetaEvent(this.oneBot.uin, "connect"))); ws.send(JSON.stringify(V11.genMetaEvent(this.oneBot.uin, "enable"))); } /** * 创建反向ws */ _createWsr(url) { const timestmap = Date.now(); const headers = { "X-Self-ID": String(this.oneBot.uin), "X-Client-Role": "Universal", "User-Agent": "OneBot", }; if (this.config.access_token) headers.Authorization = "Bearer " + this.config.access_token; const ws = new ws_1.WebSocket(url, { headers }); ws.on("error", err => { this.logger.error(err.message); }); ws.on("open", () => { this.logger.info(`反向ws(${url})连接成功。`); this.wsr.add(ws); this._webSocketHandler(ws); }); ws.on("close", code => { this.wsr.delete(ws); if (timestmap < this.timestamp) return; this.logger.warn(`反向ws(${url})被关闭,关闭码${code},将在${this.config.reconnect_interval}秒后尝试重连。`); setTimeout(() => { if (timestmap < this.timestamp) return; this._createWsr(url); }, this.config.reconnect_interval * 1000); }); } /** * 快速操作 */ _quickOperate(event, res) { if (event.post_type === "message") { if (res.reply) { if (event.message_type === "discuss") return; const action = event.message_type === "private" ? "sendPrivateMsg" : "sendGroupMsg"; const id = event.message_type === "private" ? event.user_id : event.group_id; if (typeof res.reply === "string") { if (/[CQ:music,type=.+,id=.+]/.test(res.reply)) { res.reply = res.reply.replace(",type=", ",platform="); } res.reply = this.adapter.fromCqcode("V11", res.reply); } else { if (res.reply[0].type == "music" && res.reply[0]?.data?.type) { res.reply[0].data.platform = res.reply[0].data.type; delete res.reply[0].data.type; } res.reply = this.adapter.fromSegment(this.oneBot, "V11", res.reply); } this.action[action].apply(this, [id, res.reply, res.auto_escape]); } if (event.message_type === "group") { if (res.delete) this.adapter.deleteMessage(this.oneBot.uin, "V11", [event.message_id]); if (res.kick && !event.anonymous) this.adapter.call(this.oneBot.uin, "V11", "setGroupKick", [ event.group_id, event.user_id, res.reject_add_request, ]); if (res.ban) this.adapter.call(this.oneBot.uin, "V11", "setGroupBan", [ event.group_id, event.user_id, res.ban_duration > 0 ? res.ban_duration : 1800, ]); } } if (event.post_type === "request" && "approve" in res) { const action = event.request_type === "friend" ? "setFriendAddRequest" : "setGroupAddRequest"; this.adapter.call(this.oneBot.uin, "V11", action, [ event.flag, res.approve, res.reason ? res.reason : "", !!res.block, ]); } } /** * 调用api */ async apply(req) { let { action, params, echo } = req; action = (0, utils_1.toLine)(action); let is_async = action.includes("_async"); if (is_async) action = action.replace("_async", ""); let is_queue = action.includes("_rate_limited"); if (is_queue) action = action.replace("_rate_limited", ""); if (action === "send_msg") { if (sendMsgTypes.includes(params.message_type)) action = "send_" + params.message_type + "_msg"; else if (params.user_id) action = "send_private_msg"; else if (params.group_id) action = "send_group_msg"; else throw new Error("required message_type or input (user_id/group_id)"); } const method = (0, utils_1.toHump)(action); if (Reflect.has(this.action, method)) { const ARGS = String(Reflect.get(this.action, method)) .match(/\(.*\)/)?.[0] .replace("(", "") .replace(")", "") .split(",") .filter(Boolean) .map(v => v.replace(/=.+/, "").trim()); const args = []; for (let k of ARGS) { if (Reflect.has(params, k)) { if (onebot_1.BOOLS.includes(k)) params[k] = (0, utils_1.toBool)(params[k]); if (k === "message") { if (typeof params[k] === "string") { if (/[CQ:music,type=.+,id=.+]/.test(params[k])) { params[k] = params[k].replace(",type=", ",platform="); } params[k] = this.adapter.fromCqcode("V11", params[k]); } params[k] = this.adapter.fromSegment(this.oneBot, "V11", params[k]); params["message_id"] = params[k].find(e => e.type === "reply")?.id || params["message_id"]; } args.push(params[k]); } } let ret, result; if (is_queue) { this._queue.push({ method, args }); this._runQueue(); result = V11.ok(null, 0, true); } else { try { ret = await this.action[method].apply(this, args); } catch (e) { this.logger.error(`run ${action} with args:${args.length} failed:`, e); return JSON.stringify(V11.error(e.message, echo)); } if (ret instanceof Promise) { if (is_async) { result = V11.ok(null, 0, true); } else { result = V11.ok(await ret, 0, false); } } else { result = V11.ok(await ret, 0, false); } } if (result.data instanceof Map) result.data = [...result.data.values()]; if (result.data?.message) result.data.message = this.adapter.toSegment("V11", result.data.message); if (echo) { result.echo = echo; } return JSON.stringify(result, (_, v) => (typeof v === "bigint" ? v.toString() : v)); } else throw new onebot_1.NotFoundError(); } /** * 限速队列调用 */ async _runQueue() { if (this.queue_running) return; while (this._queue.length > 0) { this.queue_running = true; const task = this._queue.shift(); const { method, args } = task; this.action[method].apply(this, args); await new Promise(resolve => { setTimeout(resolve, this.config.rate_limit_interval * 1000); }); this.queue_running = false; } } } exports.V11 = V11; (function (V11) { function ok(data, retcode = 0, pending, echo) { return { retcode: pending ? 1 : retcode, status: pending ? "async" : "ok", data, error: null, echo, }; } V11.ok = ok; function error(error, echo, retcode = 1500, wording) { return { retcode, status: "error", data: null, error, msg: error, wording, echo, }; } V11.error = error; V11.defaultConfig = { heartbeat: 3, access_token: "", post_timeout: 15, secret: "", rate_limit_interval: 4, post_message_format: "string", reconnect_interval: 3, use_http: true, enable_cors: true, use_ws: true, http_reverse: [], ws_reverse: [], }; function genMetaEvent(uin, type) { return { self_id: Number.isNaN(parseInt(uin)) ? uin : parseInt(uin), time: Math.floor(Date.now() / 1000), post_type: "meta_event", meta_event_type: "lifecycle", sub_type: type, }; } V11.genMetaEvent = genMetaEvent; })(V11 || (exports.V11 = V11 = {}));