onebots
Version:
基于icqq的多例oneBot实现
418 lines (417 loc) • 16.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.App = void 0;
exports.createOnebots = createOnebots;
exports.defineConfig = defineConfig;
const koa_1 = __importDefault(require("koa"));
const os = __importStar(require("os"));
require("reflect-metadata");
const fs = __importStar(require("fs"));
const fs_1 = require("fs");
const log4js_1 = require("log4js");
const http_1 = require("http");
const koa_static_1 = __importDefault(require("koa-static"));
const js_yaml_1 = __importDefault(require("js-yaml"));
const koa_bodyparser_1 = __importDefault(require("koa-bodyparser"));
const koa_basic_auth_1 = __importDefault(require("koa-basic-auth"));
const utils_1 = require("../utils");
const router_1 = require("./router");
const V11_1 = require("../service/V11");
const V12_1 = require("../service/V12");
const path = __importStar(require("path"));
const process_1 = __importDefault(require("process"));
class App extends koa_1.default {
static get configPath() {
return path.join(App.configDir, "config.yaml");
}
static get dataDir() {
return path.join(App.configDir, "data");
}
static get logFile() {
return path.join(App.configDir, "onebots.log");
}
get info() {
const pkg = require(path.resolve(__dirname, "../../package.json"));
const free_memory = os.freemem();
const total_memory = os.totalmem();
return {
system_platform: process_1.default.platform,
system_arch: process_1.default.arch,
system_cpus: os.cpus(),
system_version: os.version(),
system_uptime: os.uptime() * 1000,
username: os.userInfo().username,
total_memory,
free_memory,
process_id: process_1.default.pid,
process_parent_id: process_1.default.ppid,
process_cwd: process_1.default.cwd(),
process_use_memory: process_1.default.memoryUsage.rss(),
node_version: process_1.default.version,
sdk_version: pkg.version,
uptime: process_1.default.uptime() * 1000,
};
}
constructor(config = {}) {
super(config);
this.isStarted = false;
this.adapters = new Map();
this.config = (0, utils_1.deepMerge)((0, utils_1.deepClone)(App.defaultConfig), config);
this.init();
}
init() {
this.logger = (0, log4js_1.getLogger)("[OneBots]");
this.logger.level = this.config.log_level;
this.httpServer = (0, http_1.createServer)(this.callback());
this.router = new router_1.Router(this.httpServer, { prefix: this.config.path });
this.use((0, koa_bodyparser_1.default)())
.use(this.router.routes())
.use(this.router.allowedMethods())
.use(async (ctx, next) => {
const adapter = ctx.path?.slice(1)?.split("/")[0];
if (this.adapters.has(adapter))
return next();
return (0, koa_basic_auth_1.default)({
name: this.config.username,
pass: this.config.password,
})(ctx, next);
})
.use((0, koa_static_1.default)(path.resolve(__dirname, "../../dist")));
this.ws = this.router.ws("/");
this.createOneBots();
}
getLogger(patform) {
const logger = (0, log4js_1.getLogger)(`[OneBots:${patform}]`);
logger.level = this.config.log_level;
return logger;
}
getConfigMaps() {
const result = [];
for (const [key, value] of Object.entries(this.config)) {
const [adapter, ...uinArr] = key.split(".");
const uin = uinArr.join(".");
if (!uin)
continue;
result.push([adapter, uin, value]);
}
return result;
}
createOneBots() {
for (const [platform, uin, config] of this.getConfigMaps()) {
this.createOneBot(platform, uin, config);
}
}
addAccount(platform, uin, config) {
this.config[`${platform}.${uin}`] = config;
const oneBot = this.createOneBot(platform, uin, config);
oneBot.start();
(0, fs_1.writeFileSync)(App.configPath, js_yaml_1.default.dump((0, utils_1.deepClone)(this.config)));
}
updateAccount(platform, uin, config) {
const adapter = this.findOrCreateAdapter(platform);
if (!adapter)
return;
const oneBot = adapter.oneBots.get(uin);
if (!oneBot)
return this.addAccount(platform, uin, config);
const newConfig = (0, utils_1.deepMerge)(this.config[uin], config);
this.removeAccount(platform, uin);
this.addAccount(platform, uin, newConfig);
}
removeAccount(platform, uin, force) {
const adapter = this.findOrCreateAdapter(platform);
if (!adapter)
return;
const oneBot = adapter.oneBots.get(uin);
if (!oneBot)
return this.logger.warn(`未找到账号${uin}`);
oneBot.stop(force);
delete this.config[`${platform}.${uin}`];
adapter.oneBots.delete(uin);
(0, fs_1.writeFileSync)(App.configPath, js_yaml_1.default.dump(this.config));
}
createOneBot(platform, uin, config) {
const adapter = this.findOrCreateAdapter(platform, config);
if (!adapter)
return;
return adapter.createOneBot(uin, config.protocol || {}, config.versions || []);
}
get oneBots() {
return [...this.adapters.values()]
.map(adapter => {
return [...adapter.oneBots.values()];
})
.flat();
}
findOrCreateAdapter(platform, config) {
if (this.adapters.has(platform))
return this.adapters.get(platform);
const AdapterClass = App.ADAPTERS.get(platform);
if (!AdapterClass)
return this.logger.warn(`未安装适配器(${platform})`);
const adapter = new AdapterClass(this, config);
this.adapters.set(platform, adapter);
return adapter;
}
async start() {
this.httpServer.listen(this.config.port);
const fileListener = e => {
if (e === "change")
this.ws.clients.forEach(async (client) => {
client.send(JSON.stringify({
event: "system.log",
data: await (0, utils_1.readLine)(1, App.logFile),
}));
});
};
fs.watch(App.logFile, fileListener);
this.once("close", () => {
fs.unwatchFile(App.logFile, fileListener);
});
process_1.default.once("disconnect", () => {
fs.unwatchFile(App.logFile, fileListener);
});
this.ws.on("connection", async (client) => {
client.send(JSON.stringify({
event: "system.sync",
data: {
config: fs.readFileSync(App.configPath, "utf8"),
adapters: [...this.adapters.values()].map(adapter => {
return adapter.info;
}),
app: this.info,
logs: fs.existsSync(App.logFile) ? await (0, utils_1.readLine)(100, App.logFile) : "",
},
}));
client.on("message", async (raw) => {
let payload = {};
try {
payload = JSON.parse(raw.toString());
}
catch {
return;
}
switch (payload.action) {
case "system.input":
// 将流的模式切换到“流动模式”
process_1.default.stdin.resume();
// 使用以下函数来模拟输入数据
function simulateInput(data) {
process_1.default.nextTick(() => {
process_1.default.stdin.emit("data", data);
});
}
simulateInput(Buffer.from(payload.data + "\n", "utf8"));
// 模拟结束
process_1.default.nextTick(() => {
process_1.default.stdin.emit("end");
});
return true;
case "system.saveConfig":
return fs.writeFileSync(App.configPath, payload.data, "utf8");
case "system.reload":
const config = js_yaml_1.default.load(fs.readFileSync(App.configPath, "utf8"));
return this.reload(config);
case "bot.start": {
const { platform, uin } = JSON.parse(payload.data);
await this.adapters.get(platform)?.setOnline(uin);
return client.send(JSON.stringify({
event: "bot.change",
data: this.adapters.get(platform).getOneBot(uin).info,
}));
}
case "bot.stop": {
const { platform, uin } = JSON.parse(payload.data);
await this.adapters.get(platform)?.setOffline(uin);
return client.send(JSON.stringify({
event: "bot.change",
data: this.adapters.get(platform).getOneBot(uin).info,
}));
}
}
});
});
this.router.get("/list", ctx => {
ctx.body = this.oneBots.map(bot => {
return bot.info;
});
});
this.router.post("/add", (ctx, next) => {
const { uin, ...config } = (ctx.request.body || {});
try {
this.addAccount(config.platform, uin, config);
ctx.body = `添加成功`;
}
catch (e) {
ctx.body = e.message;
}
});
this.router.post("/edit", (ctx, next) => {
const { uin, ...config } = (ctx.request.body || {});
try {
this.updateAccount(config.platform, uin, config);
ctx.body = `修改成功`;
}
catch (e) {
ctx.body = e.message;
}
});
this.router.get("/remove", (ctx, next) => {
const { uin, platform, force } = ctx.request.query;
try {
this.removeAccount(String(platform), String(uin), Boolean(force));
ctx.body = `移除成功`;
}
catch (e) {
console.log(e);
ctx.body = e.message;
}
});
process_1.default.on("uncaughtException", e => {
console.error("uncaughtException", e);
});
process_1.default.on("unhandledRejection", e => {
console.error("unhandledRejection", e);
});
this.logger.mark(`server listen at http://0.0.0.0:${this.config.port}/${this.config.path ? this.config.path : ""}`);
for (const [_, adapter] of this.adapters) {
await adapter.start();
}
this.isStarted = true;
}
async reload(config) {
await this.stop();
this.config = (0, utils_1.deepMerge)((0, utils_1.deepClone)(App.defaultConfig), config);
this.createOneBots();
await this.start();
}
async stop() {
for (const [_, adapter] of this.adapters) {
await adapter.stop();
}
this.adapters.clear();
// this.ws.close()
this.httpServer.close();
this.emit("close");
this.isStarted = false;
}
}
exports.App = App;
App.configDir = path.join(os.homedir(), ".onebots");
function createOnebots(config = "config.yaml", cp = null) {
const isStartWithConfigFile = typeof config === "string";
if (isStartWithConfigFile) {
config = path.resolve(process_1.default.cwd(), config);
App.configDir = path.dirname(config);
}
if (!(0, fs_1.existsSync)(App.configDir))
(0, fs_1.mkdirSync)(App.configDir);
if (!(0, fs_1.existsSync)(App.configPath) && isStartWithConfigFile) {
(0, fs_1.copyFileSync)(path.resolve(__dirname, "../config.sample.yaml"), App.configPath);
console.log("未找到对应配置文件,已自动生成默认配置文件,请修改配置文件后重新启动");
console.log(`配置文件在: ${App.configPath}`);
process_1.default.exit();
}
if (!isStartWithConfigFile) {
(0, fs_1.writeFileSync)(App.configPath, js_yaml_1.default.dump(config));
console.log(`已自动保存配置到:${App.configPath}`);
}
if (!(0, fs_1.existsSync)(App.dataDir)) {
(0, fs_1.mkdirSync)(App.dataDir);
console.log("已为你创建数据存储目录", App.dataDir);
}
config = js_yaml_1.default.load((0, fs_1.readFileSync)(App.configPath, "utf8"));
(0, log4js_1.configure)({
appenders: {
out: {
type: "stdout",
layout: { type: "colored" },
},
files: {
type: "file",
maxLogSize: 1024 * 1024 * 50,
filename: path.join(process_1.default.cwd(), "onebots.log"),
},
},
categories: {
default: {
appenders: ["out", "files"],
level: config.log_level || "info",
},
},
disableClustering: true,
});
if (cp)
process_1.default.on("disconnect", () => cp.kill());
return new App(config);
}
function defineConfig(config) {
return config;
}
(function (App) {
App.ADAPTERS = new Map();
App.defaultConfig = {
port: 6727,
username: "admin",
password: "123456",
timeout: 30,
general: {
V11: V11_1.V11.defaultConfig,
V12: V12_1.V12.defaultConfig,
},
log_level: "info",
};
function registerAdapter(platform, adapter) {
if (!adapter)
adapter = App.loadAdapter(platform);
if (App.ADAPTERS.has(platform)) {
console.warn(`已存在对应的适配器:${platform}`);
return this;
}
App.ADAPTERS.set(platform, adapter);
}
App.registerAdapter = registerAdapter;
function loadAdapter(platform) {
const maybeNames = [
path.join(__dirname, "../adapters", platform), // 内置的
`/adapter-${platform}`, // 我写的
`onebots-adapter-${platform}`, // 别人按照规范写的
platform, // 别人写的
];
let adapter;
try {
adapter = require(path.join(__dirname, "../adapters", platform))?.default;
}
catch (e) {
console.error(`loadAdapter(${platform}) failed:${e.message}`);
}
return adapter;
}
App.loadAdapter = loadAdapter;
})(App || (exports.App = App = {}));