qq-official-bot
Version:
324 lines (323 loc) • 10.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebhookReceiver = exports.WebhookReceiverConfig = void 0;
exports.createWebhookReceiver = createWebhookReceiver;
const http_1 = require("http");
const base_1 = require("./base");
const constants_1 = require("../constants");
const ed25519_1 = require("../ed25519");
/**
* Webhook接收器配置
*/
class WebhookReceiverConfig extends base_1.BaseReceiverConfig {
constructor(options) {
super(base_1.ReceiverMode.WEBHOOK);
this.port = options.port;
this.path = options.path;
this.timeout = options.timeout || 30000;
}
validate() {
return this.port > 0 && this.port < 65536 &&
this.path.startsWith('/') &&
this.timeout > 0;
}
toJson() {
return {
mode: this.mode,
port: this.port,
path: this.path,
timeout: this.timeout
};
}
}
exports.WebhookReceiverConfig = WebhookReceiverConfig;
/**
* Webhook接收器实现
*/
class WebhookReceiver extends base_1.BaseReceiver {
constructor(config) {
const server = (0, http_1.createServer)();
super({ server });
this.session = null;
this.config = config;
this.setupWebhookEventHandlers();
this.setupServerHandlers();
}
/**
* 设置Webhook特定的事件处理器
*/
setupWebhookEventHandlers() {
this.on('start', this.handleStart.bind(this));
this.on('stop', this.handleStop.bind(this));
}
/**
* 设置服务器请求处理器
*/
setupServerHandlers() {
this.handler.server.on('request', async (req, res) => {
await this.handleHttpRequest(req, res);
});
this.handler.server.on('error', (error) => {
this.handleServerError(error);
});
this.handler.server.on('listening', () => {
this.handleServerListening();
});
this.handler.server.on('close', () => {
this.handleServerClose();
});
}
/**
* 启动Webhook服务器
*/
async start(session) {
this.session = session;
this._isStarted = true;
// 初始化Ed25519签名验证
this.handler.ed25519 = new ed25519_1.Ed25519(session.getBot().config.secret);
if (this.handler.server.listening) {
throw new Error('Webhook receiver has already started');
}
return new Promise((resolve, reject) => {
const onListening = () => {
this.handler.server.removeListener('error', onError);
resolve();
};
const onError = (error) => {
this.handler.server.removeListener('listening', onListening);
reject(error);
};
this.handler.server.once('listening', onListening);
this.handler.server.once('error', onError);
this.handler.server.listen(this.config.port);
});
}
/**
* 停止Webhook服务器
*/
async stop(session) {
this._isStarted = false;
this.handler.ed25519 = undefined;
if (!this.handler.server.listening) {
return;
}
return new Promise((resolve, reject) => {
this.handler.server.close((error) => {
if (error) {
reject(error);
}
else {
resolve();
}
});
});
}
/**
* 处理启动事件
*/
async handleStart(session) {
await this.start(session);
}
/**
* 处理停止事件
*/
async handleStop() {
await this.stop();
}
/**
* 处理HTTP请求
*/
async handleHttpRequest(req, res) {
// 检查路径是否匹配
if (req.url !== this.config.path) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
return;
}
try {
await this.handleWebhookRequest(req, res);
}
catch (error) {
this.handleRequestError(req, res, error);
}
}
/**
* 处理Webhook请求
*/
async handleWebhookRequest(req, res, fallbackData) {
if (!this.session || !this.handler.ed25519) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Server not ready');
return;
}
const bodyData = await this.resolveBodyData(req, JSON.stringify(fallbackData));
// 验证签名
const signature = req.headers['x-signature-ed25519']?.toString();
const timestamp = req.headers['x-signature-timestamp']?.toString();
if (!signature) {
this.session.getBot().logger.warn('[WebhookReceiver] 缺少签名头');
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Missing signature');
return;
}
if (!this.handler.ed25519.verify(signature, timestamp + bodyData)) {
this.session.getBot().logger.warn('[WebhookReceiver] 签名验证失败');
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Invalid signature');
return;
}
// 解析数据包
let packet;
try {
packet = JSON.parse(bodyData);
}
catch (error) {
this.session.getBot().logger.error('[WebhookReceiver] 数据包解析失败:', error);
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid JSON');
return;
}
// 处理不同类型的操作
switch (packet.op) {
case constants_1.OpCode.SIGN_VERIFY:
await this.handleSignVerify(packet, res);
break;
case constants_1.OpCode.DISPATCH:
await this.handleDispatch(packet, res);
break;
default:
this.session.getBot().logger.warn(`[WebhookReceiver] 未知的操作码: ${packet.op}`);
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Unknown operation');
}
}
/**
* 处理签名验证
*/
async handleSignVerify(packet, res) {
if (!this.session || !this.handler.ed25519)
return;
const { plain_token, event_ts } = packet.d;
const signed = this.handler.ed25519.sign(event_ts + plain_token);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
plain_token,
signature: signed
}));
this.session.getBot().logger.debug('[WebhookReceiver] 处理签名验证完成');
}
/**
* 处理事件分发
*/
async handleDispatch(packet, res) {
if (!this.session)
return;
this.session.getBot().logger.debug('[WebhookReceiver] 收到事件:', packet.t);
// 发送packet事件
this.emitPacket(packet);
// 返回成功响应
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ code: 0, message: 'success' }));
}
/**
* 解析请求体数据
*/
async resolveBodyData(req, fallbackData) {
return new Promise((resolve) => {
if (fallbackData) {
resolve(fallbackData);
return;
}
const dataArr = [];
req.on('data', (data) => {
dataArr.push(data);
});
req.on('end', () => {
resolve(Buffer.concat(dataArr).toString());
});
});
}
/**
* 处理请求错误
*/
handleRequestError(req, res, error) {
if (this.session) {
this.session.getBot().logger.error('[WebhookReceiver] 请求处理错误:', error);
}
this.emitError(error);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
}
/**
* 处理服务器错误
*/
handleServerError(error) {
if (this.session) {
this.session.getBot().logger.error('[WebhookReceiver] 服务器错误:', error);
}
this.emitError(error);
}
/**
* 处理服务器开始监听
*/
handleServerListening() {
if (this.session) {
this.session.getBot().logger.info(`[WebhookReceiver] 服务器开始监听端口: ${this.config.port}`);
}
this.emitReady();
}
/**
* 处理服务器关闭
*/
handleServerClose() {
if (this.session) {
this.session.getBot().logger.info('[WebhookReceiver] 服务器已关闭');
}
this.emitClose();
}
/**
* 处理数据包(基类抽象方法实现)
*/
handlePacket(packet) {
// Webhook接收器的数据包处理在handleWebhookRequest中进行
// 这里不需要额外处理
}
/**
* 获取接收器类型
*/
getType() {
return base_1.ReceiverMode.WEBHOOK;
}
/**
* 获取接收器配置
*/
getConfig() {
return this.config.toJson();
}
/**
* 健康检查
*/
healthCheck() {
const baseHealth = super.healthCheck();
return {
...baseHealth,
details: {
...baseHealth.details,
serverListening: this.handler.server.listening,
port: this.config.port,
path: this.config.path,
hasEd25519: !!this.handler.ed25519
}
};
}
}
exports.WebhookReceiver = WebhookReceiver;
/**
* 创建Webhook接收器的工厂函数(向后兼容)
*/
function createWebhookReceiver(port, path) {
const config = new WebhookReceiverConfig({ port, path });
return new WebhookReceiver(config);
}