el-bot
Version:
A quick qq bot framework for mirai.
376 lines (329 loc) • 9.16 kB
text/typescript
import type mongoose from 'mongoose'
import type { ElConfig, ElUserConfig } from '../config/el'
import path from 'node:path'
import process from 'node:process'
import fs from 'fs-extra'
import { createHooks } from 'hookable'
import { GroupMessage, NCWebsocket, PrivateMessage, Send, Structs } from 'node-napcat-ts'
import colors from 'picocolors'
import { createWebhooks } from '../../node/server/webhook'
import { resolveElConfig } from '../config/el'
import { connectDb } from '../db'
import { isFunction } from '../shared'
import { handleError } from '../utils/error'
import { statement } from '../utils/misc'
import { Command } from './command'
import { Plugins } from './plugins'
import { Sender } from './sender'
import { Status } from './status'
import { User } from './user'
// shared
// node
// type
import type { Plugin, PluginInstallFunction } from './plugins/class'
import consola from 'consola'
import yargs from 'yargs'
import { BotServer, createServer } from '../../node/server'
import { LiteCycleHook } from '../composition-api'
import { setCurrentInstance } from '../composition-api/lifecycle'
import { logger } from './logger'
export * from './logger'
export * from './plugins'
/**
* 创建机器人
* @param el 用户配置 | el-bot.config.ts 所在目录
*/
export async function createBot(el: ElUserConfig | string = process.cwd()) {
let elConfig: ElUserConfig
// resolve el-bot.config.ts
if (typeof el === 'string') {
const rootDir = el
const elConfigFile = path.resolve(rootDir, 'el-bot.config.ts')
if (!(await fs.exists(elConfigFile))) {
consola.error('el-bot.config.ts not found')
consola.error('Please create `el-bot.config.ts` in the root directory.')
}
const config = (await import(elConfigFile)).default as ElUserConfig
elConfig = config
}
else if (typeof el === 'object') {
elConfig = el
}
else {
throw new TypeError('`createBot` option must be a string or object')
}
return new Bot(elConfig)
}
export class Bot {
/**
* 全局配置
*/
el: ElConfig
// mirai: MiraiInstance
// 激活
active = true
/**
* 数据库,默认使用 MongoDB
*/
db?: mongoose.Connection
/**
* node-napcat-ts
*/
napcat: NCWebsocket
/**
* Server
*/
server?: BotServer
/**
* 状态
*/
status: Status
/**
* 用户系统
*/
user: User
/**
* 发送器
*/
sender: Sender
/**
* 插件系统
*/
plugins: Plugins
/**
* 已安装的插件
*/
installedPlugins = new Set()
/**
* for composition-api
*/
hooks = createHooks<LiteCycleHook>()
/**
* 面向开发者的指令系统
*/
cli = yargs()
.scriptName('el')
.usage('Usage: $0 <command> [options]')
.version()
.alias('h', 'help')
.alias('v', 'version')
.help()
/**
* 面向用户的指令系统
*/
_command: Command
/**
* 日志系统
*/
logger = logger
/**
* 是否开发模式下
*/
isDev = process.env.NODE_ENV !== 'production'
rootDir = process.cwd()
tmpDir = 'tmp/'
constructor(el: ElUserConfig) {
statement()
this.el = resolveElConfig(el)
// const setting = this.el.setting as MiraiApiHttpSetting
// this.mirai = new Mirai(setting)
this.status = new Status(this)
this.user = new User(this)
this.sender = new Sender(this)
this.plugins = new Plugins(this)
this._command = new Command(this)
this.server = createServer(this.el.server)
// this.cli = initCli(this, 'el')
// report error to qq
if (this.el.report?.enable) {
const logError = this.logger.error
this.logger.error = (...args: any) => {
const target = this.el.report?.target || {}
if (this.el.bot.devGroup) {
if (target?.group)
target.group.push(this.el.bot.devGroup)
else target.group = [this.el.bot.devGroup]
}
// this.sender.sendMessageByConfig(args.join(' '), target)
return logError(args[0], ...args.slice(1))
}
}
// init napcat
const napcatConfig = this.el.napcat
this.napcat = new NCWebsocket(napcatConfig, this.el.debug || napcatConfig.debug)
}
/**
* 机器人当前消息 快捷回复
*/
reply(rawMsg: PrivateMessage | GroupMessage, msg: Send[keyof Send][] | string, quote = false) {
const napcat = this.napcat
// 文本消息
if (typeof msg === 'string') {
msg = [Structs.text(msg)]
}
// 引用消息
if (quote) {
msg.unshift(Structs.reply(rawMsg.message_id))
}
if (rawMsg.sender.user_id) {
// 私聊
if (rawMsg.message_type === 'private') {
return napcat.send_private_msg({
user_id: rawMsg.sender.user_id,
message: msg,
})
}
// 群聊
else if (rawMsg.message_type === 'group') {
return napcat.send_group_msg({
group_id: rawMsg.group_id,
message: msg,
})
}
}
}
/**
* 启动机器人
*/
async start() {
consola.log('')
consola.start('Starting el-bot...')
// 连接数据库
if (this.el.db?.enable)
await connectDb(this, this.el.db)
try {
await this.napcat.connect()
const data = await this.napcat.get_version_info()
consola.success(`${data.app_name} ${colors.yellow(data.app_version)} ${colors.cyan(data.protocol_version)} connected!`)
}
catch (err: any) {
consola.error('NapCat by SDK 连接失败')
handleError(err)
}
// get login info
try {
const data = await this.napcat.get_login_info()
consola.info('当前登录账号:', `${colors.yellow(data.nickname)}(${colors.cyan(data.user_id)})`)
}
catch (err) {
handleError(err)
}
// reset
this.hooks.removeAllHooks()
// 加载插件
consola.log('')
setCurrentInstance(this)
await this.plugins.loadConfig()
// 自动加载自定义插件
if (this.el.bot.autoloadPlugins) {
await this.plugins.loadCustom(this.el.bot.pluginDir)
}
consola.log('')
consola.success('插件加载完成')
consola.log('')
// 监听并解析用户指令
this._command.listen()
// 启动 webhook
// if (this.el.webhook && this.el.webhook.enable) {
// try {
// this.server = this.webhook?.start()
// }
// catch (err: any) {
// handleError(err)
// }
// }
// onMessage
this.napcat.on('message', async (msg) => {
await this.hooks.callHook('onMessage', msg)
await this.hooks.callHook('onNapcatMessage', msg)
switch (msg.message_type) {
case 'private':
await this.hooks.callHook('onPrivateMessage', msg)
break
case 'group':
await this.hooks.callHook('onGroupMessage', msg)
break
}
})
// 如何解决持久运行
// 意外退出
process.on('SIGINT', () => {
/**
* 如果程序在前台运行(即,process.stdin.isTTY 为 true),那么它会在收到 SIGINT 信号时退出。如果程序在后台运行(即,process.stdin.isTTY 为 false),那么它会忽略 SIGINT 信号。
*/
if (process.stdin.isTTY) {
this.stop()
process.exit(0)
}
})
}
/**
* 停止机器人
*/
async stop() {
this.napcat.disconnect()
// 关闭数据库连接
if (this.db) {
this.db.close()
this.logger.info('[db] 关闭数据库连接')
}
// close koa server
// if (this.el.webhook && this.el.webhook.enable) {
// if (this.server) {
// this.server.close()
// this.logger.info('[webhook] 关闭 Server')
// }
// }
this.logger.success('Bye, Master!')
consola.log('')
}
/**
* 加载自定义函数插件(但不注册)
* 注册请使用 .plugin
* 与 this.plugin.use() 的区别是此部分的插件将不会显示在插件列表中
*/
use(plugin: Plugin | PluginInstallFunction, ...options: any[]) {
const installedPlugins = this.installedPlugins
if (installedPlugins.has(plugin)) {
if (this.isDev) {
this.logger.warn('插件已经被安装')
}
}
else if (plugin && isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(this, ...options)
}
else if (isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(this, ...options)
}
else if (this.isDev) {
consola.info(plugin)
this.logger.warn('插件必须是一个函数,或是带有 "install" 属性的对象。')
}
return this
}
/**
* 注册插件
* @param name 插件名称
* @param plugin 插件函数
* @param options 插件选项
*/
plugin(name: string, plugin: Plugin | PluginInstallFunction, ...options: any[]) {
const addedPlugin = isFunction(plugin)
? {
install: plugin,
}
: plugin
this.plugins.add(name, addedPlugin, ...options)
this.plugins.custom.add({
name,
})
}
/**
* 注册指令(面向用户)
*/
command(name: string) {
return this._command.command(name)
}
}