onebots
Version:
基于icqq的多例oneBot实现
760 lines (759 loc) • 29.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.V12 = void 0;
const utils_1 = require("../../utils");
const path_1 = require("path");
const onebot_1 = require("../../onebot");
const action_1 = require("./action");
const url_1 = require("url");
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const ws_1 = require("ws");
const utils_2 = require("../../utils");
const db_1 = require("../../db");
const service_1 = require("../../service");
const app_1 = require("../../server/app");
class V12 extends service_1.Service {
constructor(oneBot, config) {
super(oneBot.adapter, config);
this.oneBot = oneBot;
this.config = config;
this.version = "V12";
this.timestamp = Date.now();
this.wsr = new Set();
this.db = new db_1.JsonDB((0, path_1.join)(app_1.App.configDir, "data", `${this.oneBot.uin}_v12.jsondb`));
this.action = new action_1.Action();
this.logger = this.oneBot.adapter.getLogger(this.oneBot.uin, this.version);
}
addHistory(payload) {
return this.db.push("eventBuffer", payload);
}
shiftHistory() {
return this.db.shift("eventBuffer");
}
get history() {
return this.db.get("eventBuffer", []);
}
getFile(file_id) {
return this.db.get(`files.${file_id}`);
}
delFile(file_id) {
return this.db.delete(`files.${file_id}`);
}
saveFile(fileInfo) {
const file_id = (0, utils_2.uuid)();
this.db.set(`files.${file_id}`, fileInfo);
return file_id;
}
get files() {
const files = this.db.get("files", {});
return Object.keys(files).map(file_id => {
return {
file_id,
...files[file_id],
};
});
}
set history(value) {
this.db.set("eventBuffer", value);
}
start() {
if (this.config.use_http) {
const config = typeof this.config.use_http === "boolean" ? {} : this.config.use_http || {};
this.startHttp({
access_token: this.config.access_token,
event_enabled: true,
event_buffer_size: 10,
...config,
});
}
if (this.config.use_ws) {
const config = typeof this.config.use_ws === "boolean" ? {} : this.config.use_ws || {};
this.startWs({
access_token: this.config.access_token,
...config,
});
}
this.config.webhook.forEach(config => {
if (typeof config === "string") {
config = {
url: config,
access_token: this.config.access_token,
};
}
else {
config = {
access_token: this.config.access_token,
...config,
};
}
this.startWebhook(config);
});
this.config.ws_reverse.forEach(config => {
if (typeof config === "string") {
config = {
url: config,
access_token: this.config.access_token,
};
}
else {
config = {
access_token: this.config.access_token,
...config,
};
}
this.startWsReverse(config);
});
this.on("dispatch", unserialized => {
const serialized = JSON.stringify(unserialized, (_, v) => typeof v === "bigint" ? v.toString() : v);
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(V12.formatPayload(this.oneBot.uin, "heartbeat", {
detail_type: "heartbeat",
interval: new Date().getTime() + this.config.heartbeat * 1000,
status: this.action.getStatus.apply(this),
}));
}, this.config.heartbeat * 1000);
}
this.oneBot.on("message.receive", event => {
const payload = this.adapter.formatEventPayload(this.oneBot.uin, "V12", "message", event);
this.dispatch(payload);
});
this.oneBot.on("notice.receive", event => {
const payload = this.adapter.formatEventPayload(this.oneBot.uin, "V12", "notice", event);
this.dispatch(payload);
});
this.oneBot.on("request.receive", event => {
const payload = this.adapter.formatEventPayload(this.oneBot.uin, "V12", "request", event);
this.dispatch(payload);
});
}
startHttp(config) {
this.oneBot.app.router.all(this.path, ctx => this.httpRequestHandler(ctx, config));
this.oneBot.app.router.all(new RegExp(`^${this.path}/(.*)$`), ctx => this._httpRequestHandler(ctx, config));
this.logger.mark(`开启http服务器成功,监听:http://127.0.0.1:${this.oneBot.app.config.port}${this.path}`);
this.on("dispatch", (payload) => {
if (!["message", "notice", "request", "meta"].includes(payload.type))
return;
if (config.event_enabled) {
this.addHistory(payload);
if (config.event_buffer_size !== 0 &&
this.history.length > config.event_buffer_size)
this.shiftHistory();
}
});
}
startWebhook(config) {
const options = {
method: "POST",
timeout: config.timeout || this.config.request_timeout,
headers: {
"Content-Type": "application/json",
"User-Agent": "OneBot/12 Node-onebots/" + 12,
"X-OneBot-Version": 12,
"X-Impl": "onebots",
},
};
if (config.access_token) {
options.headers["Authorization"] = `Bearer ${config.access_token}`;
}
const protocol = config.url.startsWith("https") ? https_1.default : http_1.default;
this.on("dispatch", (unserialized) => {
try {
const serialized = JSON.stringify(unserialized, (_, v) => typeof v === "bigint" ? v.toString() : v);
protocol
.request(config.url, {
...options,
headers: {
...options.headers,
"Content-Length": Buffer.byteLength(serialized),
},
}, res => {
if (res.statusCode !== 200)
return this.logger.warn(`Webhook(${config.url})上报事件收到非200响应:` +
res.statusCode);
let data = "";
res.setEncoding("utf-8");
res.on("data", chunk => (data += chunk));
res.on("end", () => {
this.logger.debug(`收到Webhook响应 ${res.statusCode} :` + data);
if (!data)
return;
try {
this._quickOperate(unserialized, JSON.parse(data));
}
catch (e) {
this.logger.error(`快速操作遇到错误:` + e.message);
}
});
})
.on("error", err => {
this.logger.error(`Webhook(${config.url})上报事件失败:` + err.message);
})
.end(serialized, () => {
this.logger.debug(`Webhook(${config.url})上报事件成功: ` + serialized);
});
}
catch (e) {
this.logger.error(`Webhook(${config.url})上报失败:` + e.message);
}
});
if (config.get_latest_actions) {
const interval = config.get_latest_actions && typeof config.get_latest_actions === "object"
? config.get_latest_actions.interval * 1000
: 1000 * 60;
setInterval(() => {
try {
const actionPath = typeof config.get_latest_actions === "string"
? config.get_latest_actions
: typeof config.get_latest_actions === "boolean"
? "get_latest_actions"
: config.get_latest_actions.path || "/get_latest_actions";
protocol
.request(`${config.url}${actionPath}`, {
...options,
method: "GET",
headers: {
...options.headers,
},
}, res => {
if (res.statusCode !== 200)
return this.logger.warn(`Webhook(${config.url})获取动作队列收到非200响应:` +
res.statusCode);
let data = "";
res.setEncoding("utf-8");
res.on("data", chunk => (data += chunk));
res.on("end", () => {
this.logger.info(`获取动作队列响应 ${res.statusCode} :` + data);
if (!data)
return;
try {
this.runActions(JSON.parse(data));
}
catch (e) {
this.logger.error(`执行动作报错:` + e.message);
}
});
})
.on("error", err => {
this.logger.error(`Webhook(${config.url})获取动作队列失败:` + err.message);
})
.end();
}
catch (e) {
this.logger.error(`Webhook(${config.url})获取动作队列失败:` + e.message);
}
}, interval);
}
}
runActions(actions) {
for (const action of actions) {
this.apply(action);
}
}
startWs(config) {
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 (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 ${config.access_token}`)
return ws.close(401, "wrong access token");
}
this._webSocketHandler(ws);
});
}
startWsReverse(config) {
this._createWsr(config.url, config);
}
async stop(force) {
this.wss.close();
for (const ws of this.wsr) {
ws.close();
}
}
format(event, ...args) {
const data = typeof args[0] === "object" ? args.shift() || {} : {};
data.type = data.post_type;
if (!data.type) {
data.type = "meta";
data.detail_type = "online";
if (data.image) {
data.type = "login";
data.detail_type = "qrcode";
}
else if (data.url) {
data.type = "login";
data.detail_type = "slider";
if (data.phone) {
data.detail_type = "device";
}
}
else if (data.message) {
data.type = "login";
data.detial_type = "error";
}
}
if (data.type === "notice") {
switch (data.detail_type) {
case "friend":
if (["increase", "decrease"].includes(data.sub_type))
data.detail_type = "friend_" + data.sub_type;
else if (data.sub_type === "recall")
data.detail_type = "private_message_delete";
break;
case "group":
if (["increase", "decrease"].includes(data.sub_type))
data.detail_type = "group_member_" + data.sub_type;
else if (data.sub_type === "recall")
data.detail_type = "group_message_delete";
}
}
if (data.type === "system")
data.type = "meta";
data.alt_message = data.raw_message;
data.self = this.action.getSelfInfo.apply(this);
if (!data.detail_type)
data.detail_type =
data.message_type || data.notice_type || data.request_type || data.system_type;
data.message =
data.type === "message" ? this.adapter.toSegment("V12", data.message) : data.message;
return V12.formatPayload(this.oneBot.uin, event, data);
}
system_online(data) { }
async dispatch(data) {
const payload = {
id: (0, utils_2.uuid)(),
impl: "onebots",
version: 12,
platform: this.oneBot.platform,
self: {
platform: this.oneBot.platform,
user_id: `${this.oneBot.uin}`,
},
};
Object.assign(payload, (0, utils_2.transformObj)(data, (key, value) => {
if (![
"user_id",
"group_id",
"discuss_id",
"member_id",
"channel_id",
"guild_id",
].includes(key))
return value;
return value + "";
}), {
self_id: `${this.oneBot.uin}`,
self: {
platform: this.oneBot.platform,
user_id: `${this.oneBot.uin}`,
},
});
if (payload.message && payload.type === "message") {
payload.message = this.adapter.transformMessage(this.oneBot.uin, "V12", payload.message);
}
if (!this.filterFn(payload))
return;
this.emit("dispatch", payload);
}
transformMedia(segment) {
const file = this.getFile(segment.data.file_id);
if (file && file.data)
return {
type: segment.type,
data: {
...segment.data,
file_id: `base64://${file.data}`,
},
};
return segment;
}
async apply(req) {
let { action = "", params = {}, echo } = req;
action = (0, utils_2.toLine)(action);
let is_async = action.includes("_async");
if (is_async)
action = action.replace("_async", "");
if (action === "send_message") {
if (["private", "group", "discuss", "direct", "guild"].includes(params.detail_type)) {
action = "send_" + params.detail_type + "_msg";
}
else if (params.user_id)
action = "send_private_Msg";
else if (params.group_id)
action = "send_group_msg";
else if (params.discuss_id)
action = "send_discuss_msg";
else if (params.channel_id)
action = "send_guild_msg";
else if (params.guild_id)
action = "send_direct_msg";
else
throw new Error("required detail_type or input (user_id/group_id/guild_id/channel_id)");
}
const method = (0, utils_2.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_2.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("V12", params[k]);
}
params[k] = this.adapter.fromSegment(this.oneBot, "V12", params[k]);
}
args.push(params[k]);
}
}
let ret, result;
try {
ret = this.action[method].apply(this, args);
}
catch (e) {
this.logger.error(e);
return "API 调用异常";
}
if (ret instanceof Promise) {
if (is_async) {
result = V12.success(null, 0);
}
else {
result = V12.success(await ret, 0);
}
}
else {
result = V12.success(ret, 0);
}
if (result.data instanceof Map)
result.data = [...result.data.values()];
if (result.data?.message)
result.data.message = this.adapter.toSegment("V12", 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 httpAuth(ctx, config) {
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 (config.access_token) {
if (ctx.headers["authorization"]) {
if (ctx.headers["authorization"] !== `Bearer ${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", "*");
}
async httpRequestHandler(ctx, config) {
if (await this.httpAuth(ctx, config))
return;
if (ctx.method === "GET") {
try {
const ret = await this.apply(ctx.query);
ctx.res.writeHead(200).end(ret);
}
catch (e) {
this.logger.error(e);
ctx.res.writeHead(500).end(e.stack || e.message);
}
}
else if (ctx.method === "POST") {
try {
const params = {
...(ctx.request.query || {}),
...(ctx.request.body || {}),
};
const ret = await this.apply(params);
ctx.res.writeHead(200).end(ret);
}
catch (e) {
this.logger.error(e);
ctx.res.writeHead(500).end(e.stack || e.message);
}
}
else {
ctx.res.writeHead(405).end();
}
}
async _httpRequestHandler(ctx, config) {
if (await this.httpAuth(ctx, config))
return;
const url = new url_1.URL(ctx.url, `http://127.0.0.1`);
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) {
this.logger.error(e);
ctx.res.writeHead(500).end(e.stack || 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) {
this.logger.error(e);
ctx.res.writeHead(500).end(e.stack || e.message);
}
}
else {
ctx.res.writeHead(405).end();
}
}
/**
* 快速操作
*/
_quickOperate(event, res) {
if (event.type === "message") {
if (res.reply) {
if (event.detail_type === "discuss")
return;
const action = event.detail_type === "private" ? "sendPrivateMsg" : "sendGroupMsg";
const id = event.detail_type === "private" ? event.user_id : event.group_id;
this.action[action].apply(this, [id, res.reply]);
}
if (event.detail_type === "group") {
if (res.delete)
this.adapter.call(this.oneBot.uin, "V12", "deleteMsg", [event.message_id]);
if (res.kick && !event.anonymous)
this.adapter.call(this.oneBot.uin, "V12", "setGroupKick", [
event.group_id,
event.user_id,
res.reject_add_request,
]);
if (res.ban)
this.adapter.call(this.oneBot.uin, "V12", "setGroupBan", [
event.group_id,
event.user_id,
res.ban_duration > 0 ? res.ban_duration : 1800,
]);
}
}
if (event.type === "request" && "approve" in res) {
const action = event.detail_type === "friend" ? "setFriendAddRequest" : "setGroupAddRequest";
this.adapter.call(this.oneBot.uin, "V12", action, [
event.flag,
res.approve,
res.reason ? res.reason : "",
!!res.block,
]);
}
}
/**
* 创建反向ws
*/
_createWsr(url, config) {
const timestmap = Date.now();
let remoteUrl = url;
if (config.access_token)
remoteUrl += `?access_token=${config.access_token}`;
const headers = {
"User-Agent": `OneBot/12 (${this.oneBot.platform}) onebots/${utils_1.version}`,
};
if (config.access_token)
headers.Authorization = "Bearer " + config.access_token;
const ws = new ws_1.WebSocket(remoteUrl, ["12.OneBots"], { 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, config);
}, this.config.reconnect_interval * 1000);
});
}
/**
* 处理ws消息
*/
_webSocketHandler(ws) {
ws.on("message", async (msg) => {
this.logger.debug(" 收到ws消息:" + msg);
var data;
try {
data = JSON.parse(String(msg));
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: "ok",
data: null,
message: 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 = 10002;
message = "不支持的api";
}
else {
code = 10003;
this.logger.debug(e);
message = e?.message || "请求格式错误";
}
ws.send(JSON.stringify({
retcode: code,
status: "failed",
data: null,
error: {
code,
message,
},
echo: data?.echo,
}));
}
});
this.dispatch(V12.formatPayload(this.oneBot.uin, "connect", {
detail_type: "connect",
type: "meta",
version: this.action.getVersion.apply(this),
}));
this.dispatch(V12.formatPayload(this.oneBot.uin, "status_update", {
detail_type: "status_update",
status: this.action.getStatus.apply(this),
}));
}
}
exports.V12 = V12;
(function (V12) {
V12.defaultConfig = {
heartbeat: 3,
access_token: "",
request_timeout: 15,
reconnect_interval: 3,
enable_cors: true,
enable_reissue: false,
use_http: true,
use_ws: true,
webhook: [],
ws_reverse: [],
};
function success(data, retcode = 0, echo) {
return {
retcode,
status: retcode === 0 ? "ok" : "failed",
data,
message: "",
echo,
};
}
V12.success = success;
function error(message, retcode = 10001, echo) {
return {
retcode,
status: "failed",
data: null,
message,
echo,
};
}
V12.error = error;
function formatPayload(uin, type, data) {
return {
self_id: uin,
time: Math.floor(Date.now() / 1000),
detail_type: type,
type: "meta",
sub_type: "",
...data,
group: data["group"]?.info,
friend: data["friend"]?.info,
member: data["member"]?.info,
};
}
V12.formatPayload = formatPayload;
})(V12 || (exports.V12 = V12 = {}));