@zhin.js/adapter-onebot11
Version:
zhin adapter for onebot11
415 lines • 15.9 kB
JavaScript
import WebSocket from 'ws';
import { EventEmitter } from "events";
import { Adapter, usePlugin, Message, registerAdapter, segment, useContext } from 'zhin.js';
import { clearInterval } from "node:timers";
const plugin = usePlugin();
// ============================================================================
// OneBot11 适配器实现
// ============================================================================
export class OneBot11WsClient extends EventEmitter {
$config;
$connected;
ws;
reconnectTimer;
heartbeatTimer;
requestId = 0;
pendingRequests = new Map();
constructor($config) {
super();
this.$config = $config;
this.$connected = false;
}
async $connect() {
return new Promise((resolve, reject) => {
let wsUrl = this.$config.url;
const headers = {};
if (this.$config.access_token) {
headers['Authorization'] = `Bearer ${this.$config.access_token}`;
}
this.ws = new WebSocket(wsUrl, { headers });
this.ws.on('open', () => {
this.$connected = true;
if (!this.$config.access_token)
plugin.logger.warn(`missing 'access_token', your OneBot protocol is not safely`);
this.startHeartbeat();
resolve();
});
this.ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleWebSocketMessage(message);
}
catch (error) {
this.emit('error', error);
}
});
this.ws.on('close', (code, reason) => {
this.$connected = false;
reject({ code, reason });
this.scheduleReconnect();
});
this.ws.on('error', (error) => {
reject(error);
});
});
}
async $disconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
}
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
// 清理所有待处理的请求
for (const [id, request] of this.pendingRequests) {
clearTimeout(request.timeout);
request.reject(new Error('Connection closed'));
}
this.pendingRequests.clear();
if (this.ws) {
this.ws.close();
this.ws = undefined;
}
}
$formatMessage(onebotMsg) {
const message = Message.from(onebotMsg, {
$id: onebotMsg.message_id.toString(),
$adapter: 'onebot11',
$bot: `${this.$config.name}`,
$sender: {
id: onebotMsg.user_id.toString(),
name: onebotMsg.user_id.toString()
},
$channel: {
id: (onebotMsg.group_id || onebotMsg.user_id).toString(),
type: onebotMsg.group_id ? 'group' : 'private'
},
$content: onebotMsg.message,
$raw: onebotMsg.raw_message,
$timestamp: onebotMsg.time,
$recall: async () => {
await this.$recallMessage(message.$id);
},
$reply: async (content, quote) => {
if (quote)
content.unshift({ type: 'reply', data: { message_id: message.$id } });
return await this.$sendMessage({
...message.$channel,
context: 'onebot11',
bot: `${this.$config.name}`,
content
});
}
});
return message;
}
async $sendMessage(options) {
options = await plugin.app.handleBeforeSend(options);
const messageData = {
message: options.content
};
if (options.type === 'group') {
const result = await this.callApi('send_group_msg', {
group_id: parseInt(options.id),
...messageData
});
plugin.logger.info(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
return result.message_id.toString();
}
else if (options.type === 'private') {
const result = await this.callApi('send_private_msg', {
user_id: parseInt(options.id),
...messageData
});
plugin.logger.info(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
return result.message_id.toString();
}
else {
throw new Error('Either group_id or user_id must be provided');
}
return '';
}
async $recallMessage(id) {
await this.callApi('delete_msg', {
message_id: parseInt(id)
});
}
async callApi(action, params = {}) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket is not connected');
}
const echo = `req_${++this.requestId}`;
const message = {
action,
params,
echo
};
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingRequests.delete(echo);
reject(new Error(`API call timeout: ${action}`));
}, 30000); // 30秒超时
this.pendingRequests.set(echo, { resolve, reject, timeout });
this.ws.send(JSON.stringify(message));
});
}
handleWebSocketMessage(message) {
// 处理API响应
if (message.echo && this.pendingRequests.has(message.echo)) {
const request = this.pendingRequests.get(message.echo);
this.pendingRequests.delete(message.echo);
clearTimeout(request.timeout);
const response = message;
if (response.status === 'ok') {
return request.resolve(response.data);
}
return request.reject(new Error(`API error: ${response.retcode}`));
}
// 处理事件消息
if (message.post_type === 'message') {
this.handleOneBot11Message(message);
}
else if (message.post_type === 'meta_event' && message.meta_event_type === 'heartbeat') {
// 心跳消息,暂时忽略
}
}
handleOneBot11Message(onebotMsg) {
const message = this.$formatMessage(onebotMsg);
plugin.dispatch('message.receive', message);
plugin.logger.info(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
plugin.dispatch(`message.${message.$channel.type}.receive`, message);
}
startHeartbeat() {
const interval = this.$config.heartbeat_interval || 30000;
this.heartbeatTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
}
}, interval);
}
scheduleReconnect() {
if (this.reconnectTimer) {
return;
}
const interval = this.$config.reconnect_interval || 5000;
this.reconnectTimer = setTimeout(async () => {
this.reconnectTimer = undefined;
try {
await this.$connect();
}
catch (error) {
this.emit('error', new Error(`Reconnection failed: ${error}`));
this.scheduleReconnect();
}
}, interval);
}
}
export class OneBot11WsServer extends EventEmitter {
router;
$config;
$connected;
#wss;
#clientMap = new Map();
heartbeatTimer;
requestId = 0;
pendingRequests = new Map();
constructor(router, $config) {
super();
this.router = router;
this.$config = $config;
this.$connected = false;
}
async $connect() {
if (!this.$config.access_token)
plugin.logger.warn(`missing 'access_token', your OneBot protocol is not safely`);
this.#wss = this.router.ws(this.$config.path, {
verifyClient: (info) => {
const { req: { headers }, } = info;
const authorization = headers['authorization'] || '';
if (this.$config.access_token && authorization !== `Bearer ${this.$config.access_token}`) {
plugin.logger.error('鉴权失败');
return false;
}
return true;
}
});
this.$connected = true;
plugin.logger.info(`ws server start at path:${this.$config.path}`);
this.#wss.on('connection', (client, req) => {
this.startHeartbeat();
plugin.logger.info(`已连接到协议端:${req.socket.remoteAddress}`);
client.on('error', err => {
plugin.logger.error('连接出错:', err);
});
client.on('close', code => {
plugin.logger.error(`与连接端(${req.socket.remoteAddress})断开,错误码:${code}`);
for (const [key, value] of this.#clientMap) {
if (client === value)
this.#clientMap.delete(key);
}
});
client.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
this.handleWebSocketMessage(client, message);
}
catch (error) {
this.emit('error', error);
}
});
});
}
async $disconnect() {
this.#wss?.close();
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
delete this.heartbeatTimer;
}
}
$formatMessage(onebotMsg) {
const message = Message.from(onebotMsg, {
$id: onebotMsg.message_id.toString(),
$adapter: 'onebot11',
$bot: `${this.$config.name}`,
$sender: {
id: onebotMsg.user_id.toString(),
name: onebotMsg.user_id.toString()
},
$channel: {
id: [onebotMsg.self_id, (onebotMsg.group_id || onebotMsg.user_id)].join(':'),
type: onebotMsg.group_id ? 'group' : 'private'
},
$content: onebotMsg.message,
$raw: onebotMsg.raw_message,
$timestamp: onebotMsg.time,
$recall: async () => {
await this.$recallMessage(message.$id);
},
$reply: async (content, quote) => {
if (!Array.isArray(content))
content = [content];
if (quote)
content.unshift({ type: 'reply', data: { message_id: message.$id } });
return await this.$sendMessage({
...message.$channel,
context: 'onebot11',
bot: `${this.$config.name}`,
content
});
}
});
return message;
}
async $sendMessage(options) {
options = await plugin.app.handleBeforeSend(options);
const messageData = {
message: options.content
};
if (options.type === 'group') {
const [self_id, id] = options.id.split(':');
const result = await this.callApi(self_id, 'send_group_msg', {
group_id: parseInt(id),
...messageData
});
plugin.logger.info(`${this.$config.name} send ${options.type}(${id}):${segment.raw(options.content)}`);
return result.message_id.toString();
}
else if (options.type === 'private') {
const [self_id, id] = options.id.split(':');
const result = await this.callApi(self_id, 'send_private_msg', {
user_id: parseInt(id),
...messageData
});
plugin.logger.info(`${this.$config.name} send ${options.type}(${id}):${segment.raw(options.content)}`);
return result.message_id.toString();
}
else {
throw new Error('Either group_id or user_id must be provided');
}
return '';
}
async $recallMessage(id) {
const [self_id, message_id] = id.split(':');
await this.callApi(self_id, 'delete_msg', {
message_id: parseInt(message_id)
});
}
async callApi(self_id, action, params = {}) {
const client = this.#clientMap.get(self_id);
if (!client || client.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket is not connected');
}
const echo = `req_${++this.requestId}`;
const message = {
action,
params,
echo
};
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingRequests.delete(echo);
reject(new Error(`API call timeout: ${action}`));
}, 30000); // 30秒超时
this.pendingRequests.set(echo, { resolve, reject, timeout });
client.send(JSON.stringify(message));
});
}
handleWebSocketMessage(client, message) {
// 处理API响应
if (message.echo && this.pendingRequests.has(message.echo)) {
const request = this.pendingRequests.get(message.echo);
this.pendingRequests.delete(message.echo);
clearTimeout(request.timeout);
const response = message;
if (response.status === 'ok') {
return request.resolve(response.data);
}
return request.reject(new Error(`API error: ${response.retcode}`));
}
switch (message.post_type) {
case 'message':
return this.handleMessage(message);
case 'meta_event':
return this.handleMetaEvent(client, message);
}
// 处理事件消息
if (message.post_type === 'message') {
}
else if (message.post_type === 'meta_event' && message.meta_event_type === 'heartbeat') {
// 心跳消息,暂时忽略
}
}
handleMetaEvent(client, message) {
switch (message.sub_type) {
case 'heartbeat':
break;
case 'connect':
this.#clientMap.set(message.self_id, client);
plugin.logger.info(`client ${message.self_id} of ${this.$config.name} by ${this.$config.context} connected`);
break;
}
}
handleMessage(onebotMsg) {
const message = this.$formatMessage(onebotMsg);
plugin.dispatch('message.receive', message);
plugin.logger.info(`${this.$config.name} recv ${message.$channel.type}(${onebotMsg.group_id || onebotMsg.user_id}):${segment.raw(message.$content)}`);
plugin.dispatch(`message.${message.$channel.type}.receive`, message);
}
startHeartbeat() {
const interval = this.$config.heartbeat_interval || 30000;
this.heartbeatTimer = setInterval(() => {
for (const client of this.#wss?.clients || []) {
if (client && client.readyState === WebSocket.OPEN) {
client.ping();
}
}
}, interval);
}
}
registerAdapter(new Adapter('onebot11', OneBot11WsClient));
useContext('router', (router) => {
registerAdapter(new Adapter('onebot11.wss', (c) => new OneBot11WsServer(router, c)));
});
//# sourceMappingURL=index.js.map