2024-09-28 12:54:02 +08:00

206 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import http from 'node:http'
import cors from 'cors'
import crypto from 'node:crypto'
import express, { Express, Request, Response, NextFunction } from 'express'
import { BaseAction } from '../action/BaseAction'
import { Context } from 'cordis'
import { llonebotError, selfInfo } from '@/common/globalVars'
import { OB11Response } from '../action/OB11Response'
import { OB11Message } from '../types'
import { OB11BaseEvent } from '../event/OB11BaseEvent'
import { handleQuickOperation, QuickOperationEvent } from '../helper/quickOperation'
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
import { Dict } from 'cosmokit'
class OB11Http {
private readonly expressAPP: Express
private server?: http.Server
constructor(protected ctx: Context, public config: OB11Http.Config) {
this.expressAPP = express()
// 添加 CORS 中间件
this.expressAPP.use(cors())
this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' }))
this.expressAPP.use((req, res, next) => {
// 兼容处理没有带content-type的请求
req.headers['content-type'] = 'application/json'
const originalJson = express.json({ limit: '5000mb' })
// 调用原始的express.json()处理器
originalJson(req, res, (err) => {
if (err) {
ctx.logger.error('Error parsing JSON:', err)
return res.status(400).send('Invalid JSON')
}
next()
})
})
this.expressAPP.use((req, res, next) => this.authorize(req, res, next))
this.expressAPP.use((req, res, next) => this.handleRequest(req, res, next))
}
public start() {
if (this.server) return
try {
this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`LLOneBot server 已启动`)
})
const host = this.config.listenLocalhost ? '127.0.0.1' : '0.0.0.0'
this.server = this.expressAPP.listen(this.config.port, host, () => {
this.ctx.logger.info(`HTTP server started ${host}:${this.config.port}`)
})
llonebotError.httpServerError = ''
} catch (e) {
this.ctx.logger.error('HTTP服务启动失败', e)
llonebotError.httpServerError = 'HTTP服务启动失败, ' + e
}
}
public stop() {
return new Promise<boolean>((resolve) => {
llonebotError.httpServerError = ''
if (this.server) {
this.server.close((err) => {
if (err) {
return resolve(false)
}
resolve(true)
})
this.server = undefined
} else {
resolve(true)
}
})
}
public updateConfig(config: Partial<OB11Http.Config>) {
Object.assign(this.config, config)
}
private authorize(req: Request, res: Response, next: NextFunction) {
const serverToken = this.config.token
if (!serverToken) return next()
let clientToken = ''
const authHeader = req.get('authorization')
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()!
this.ctx.logger.info('receive http header token', clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
clientToken = req.query.access_token[0].toString()
} else {
clientToken = req.query.access_token.toString()
}
this.ctx.logger.info('receive http url token', clientToken)
}
if (clientToken !== serverToken) {
res.status(403).json({ message: 'token verify failed!' })
} else {
next()
}
}
private async handleRequest(req: Request, res: Response, next: NextFunction) {
if (req.path === '/') return next()
let payload = req.body
if (req.method === 'GET') {
payload = req.query
} else if (req.query) {
payload = { ...req.query, ...req.body }
}
this.ctx.logger.info('收到 HTTP 请求', req.url, payload)
const action = this.config.actionMap.get(req.path.replaceAll('/', ''))
if (action) {
try {
res.json(await action.handle(payload))
} catch (e) {
res.json(OB11Response.error((e as Error).stack!.toString(), 200))
}
} else {
res.status(404).json(OB11Response.error('API 不存在', 404))
}
}
}
namespace OB11Http {
export interface Config {
port: number
token?: string
actionMap: Map<string, BaseAction<unknown, unknown>>
listenLocalhost: boolean
}
}
class OB11HttpPost {
private disposeInterval?: () => void
constructor(protected ctx: Context, public config: OB11HttpPost.Config) {
}
public start() {
if (this.config.enableHttpHeart && !this.disposeInterval) {
this.disposeInterval = this.ctx.setInterval(() => {
// ws的心跳是ws自己维护的
this.emitEvent(new OB11HeartbeatEvent(selfInfo.online!, true, this.config.heartInterval))
}, this.config.heartInterval)
}
}
public stop() {
this.disposeInterval?.()
}
public async emitEvent(event: OB11BaseEvent | OB11Message) {
const msgStr = JSON.stringify(event)
const headers: Dict = {
'Content-Type': 'application/json',
'x-self-id': selfInfo.uin,
}
if (this.config.secret) {
const hmac = crypto.createHmac('sha1', this.config.secret)
hmac.update(msgStr)
const sig = hmac.digest('hex')
headers['x-signature'] = 'sha1=' + sig
}
for (const host of this.config.hosts) {
fetch(host, {
method: 'POST',
headers,
body: msgStr,
}).then(
async (res) => {
if (event.post_type) {
this.ctx.logger.info(`HTTP 事件上报: ${host}`, event.post_type, res.status)
}
try {
const resJson = await res.json()
this.ctx.logger.info(`HTTP 事件上报后返回快速操作:`, JSON.stringify(resJson))
handleQuickOperation(this.ctx, event as QuickOperationEvent, resJson).catch(e => this.ctx.logger.error(e))
} catch (e) {
//log(`新消息事件HTTP上报没有返回快速操作不需要处理`)
}
},
(err) => {
this.ctx.logger.error(`HTTP 事件上报失败: ${host}`, err, event)
},
).catch(e => this.ctx.logger.error(e))
}
}
public updateConfig(config: Partial<OB11HttpPost.Config>) {
Object.assign(this.config, config)
}
}
namespace OB11HttpPost {
export interface Config {
hosts: string[]
secret?: string
enableHttpHeart?: boolean
heartInterval: number
}
}
export { OB11Http, OB11HttpPost }