UNPKG

koishi-plugin-adapter-iirose

Version:
1,038 lines (911 loc) 27.4 kB
import { Context, sleep, Universal } from 'koishi'; import * as zlib from 'node:zlib'; import { getMd5Password, comparePassword, md5 } from './password'; import { startEventsServer, stopEventsServer } from './utils'; import { decoderMessage } from '../decoder/decoderMessage'; import { IIROSE_Bot } from '../bot/bot'; import { decoder } from '../decoder'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; export class WsClient { private event: (() => boolean)[] = []; private ctx: Context; private bot: IIROSE_Bot; private isStarting: boolean = false; private isStarted: boolean = false; private disposed: boolean = false; live: (() => void) | null = null; private reconnectTimer: (() => void) | null = null; loginObj: { r?: string; // roomId // 机器人上线的房间唯一标识 n?: string; // username // 机器人用户名 p?: string; // hashedPassword // 机器人密码md5 st?: string; // botStatus // 账号状态 mo?: string; // signature // 机器人签名 mb?: string; // 神秘参数 作用未知 mu?: string; // 关系到服务器给不给你媒体信息 // 神秘参数 作用未知 lr?: string; // oldRoomId // 离开的房间唯一标识 rp?: string; // roomPassword // 目标房间密码 fp?: string; // hashed username // @(机器人用户名md5) // 游客专用 i?: string, // 头像 nc?: string, // 颜色 s?: string, // 性别 uid?: string, // 唯一标识 li?: string, // 重复游客ID才需要 la?: string, // 注册地址 vc?: string, // 网页客户端版本号 }; firstLogin: boolean = false; loginSuccess: boolean = false; isReconnecting: boolean = false; constructor(ctx: Context, bot: IIROSE_Bot) { this.ctx = ctx; this.bot = bot; // 重置状态 this.isStarting = false; this.isStarted = false; this.disposed = false; this.live = null; this.reconnectTimer = null; this.event = []; } setDisposing(disposing: boolean) { this.disposed = disposing; } /** * 准备ws通信 * @returns */ async prepare() { const iiroseList = ['m1', 'm2', 'm8', 'm9', 'm']; let faseter = 'www'; let maximumSpeed = 100000; let allErrors: boolean; let retryCount = 0; const maxRetries = this.bot.config.maxRetries; do { // 检查是否正在停用 if (this.disposed) { return; } allErrors = true; const speedTests: Promise<{ index: string, speed: number | 'error'; }>[] = []; // 并行测试所有服务器 for (let webIndex of iiroseList) { speedTests.push( this.getLatency(`wss://${webIndex}.iirose.com:8778`) .then(speed => ({ index: webIndex, speed })) .catch(() => ({ index: webIndex, speed: 'error' as const })) ); } try { const results = await Promise.race([ Promise.allSettled(speedTests).then(settledResults => settledResults.map(result => result.status === 'fulfilled' ? result.value : { index: '', speed: 'error' as const } ).filter(r => r.index !== '') ), new Promise<{ index: string, speed: 'error'; }[]>(resolve => this.ctx.setTimeout(() => resolve(iiroseList.map(index => ({ index, speed: 'error' as const }))), 5000) ) ]); // 检查是否正在停用 if (this.disposed) { return; } // 找到最快的可用服务器 for (const result of results) { if (result.speed !== 'error') { allErrors = false; if (maximumSpeed > result.speed) { faseter = result.index; maximumSpeed = result.speed; } } } // 如果找到了可用的服务器,立即使用,不等待其他测试完成 if (!allErrors) { break; // 跳出重试循环 } } catch (error) { this.bot.loggerWarn('服务器测试过程中出现错误:', error); } if (allErrors) { retryCount++; if (retryCount >= maxRetries) { this.bot.loggerError('达到最大重试次数,停止连接...'); throw new Error('所有服务器都无法连接,达到最大重试次数'); } this.bot.loggerWarn('所有服务器都无法连接,将在5秒后重试...'); let cancelled = false; await new Promise<void>((resolve) => { let count = 0; const dispose = this.ctx.setInterval(() => { if (this.disposed) { dispose(); this.bot.logInfo('websocket准备:插件正在停用,取消连接'); cancelled = true; resolve(); return; } count++; if (count >= 50) { // 5秒 = 50 * 100ms dispose(); resolve(); } }, 100); }); // 如果被取消,立即返回 if (cancelled) { return; } } // 再次检查 if (this.disposed) { this.bot.logInfo('websocket准备:插件正在停用,取消连接'); return; } } while (allErrors && !this.disposed && retryCount < maxRetries); let socket; try { if (!faseter) { faseter = 'www'; } const targetUrl = `wss://${faseter}.iirose.com:8778`; this.bot.loggerInfo(`找到可用服务器: ${targetUrl}, 延迟: ${maximumSpeed}ms`); socket = this.ctx.http.ws(targetUrl); // 设置二进制类型 socket.binaryType = 'arraybuffer'; const dispose = this.ctx.on('dispose', () => { if (socket && socket.readyState === 1) // WebSocket.OPEN { socket.close(); } dispose(); }); } catch (error) { if (this.disposed) { return; } this.bot.loggerError('websocket连接创建失败:', error); throw error; // 重新抛出错误,让上层处理重试 } this.bot.socket = socket; // socket = this.socket // this.socket.binaryType = 'arraybuffer' const roomIdReg = /\s*\[_([\\s\\S]+)_\]\s*/; const userNameReg = /\s*\[\\*([\\s\\S]+)\\*\]\s*/; const roomIdConfig = this.bot.config.roomId; const userNameConfig = this.bot.config.usename; let username = (userNameReg.test(userNameConfig)) ? userNameConfig.match(userNameReg)?.[1] : userNameConfig; let room = (roomIdReg.test(roomIdConfig)) ? roomIdConfig.match(roomIdReg)?.[1] : roomIdConfig; // 蔷薇游客登陆报文 // this.loginObj = { // r: room || this.bot.config.roomId, // n: "‌", // i: "cartoon/600215", // // nc: "3d5d58", // // s: "1", // // st: "n", // // mo: "", // // uid: "X000000000000", // // li: "152249236006", // // mb: "", // // mu: "01", // // la: "MY", // // vc: "1120", // fp: `@${md5(``)}` // }; if (this.bot.config.smStart && comparePassword(this.bot.config.smPassword, 'ec3a4ac482b483ac02d26e440aa0a948')) { this.loginObj = { r: this.bot.config.smRoom, n: this.bot.config.smUsername, i: this.bot.config.smImage, nc: this.bot.config.smColor, s: this.bot.config.smGender, st: this.bot.config.smst, mo: this.bot.config.smmo, uid: this.bot.config.smUid, li: this.bot.config.smli, mb: this.bot.config.smmb, mu: this.bot.config.smmu, la: this.bot.config.smLocation, vc: this.bot.config.smvc, fp: `@${md5(this.bot.config.smUsername)}` }; this.bot.loggerInfo('已启用蔷薇游客模式'); } else { const hashedPassword = getMd5Password(this.bot.config.password); if (!hashedPassword) { this.bot.loggerError('登录失败:密码不能为空'); throw new Error('密码不能为空'); } this.loginObj = { r: room || this.bot.config.roomId, n: username || this.bot.config.usename, p: hashedPassword, st: this.bot.config.botStatus, mo: this.bot.config.signature, mb: '', mu: '01', lr: this.bot.config.oldRoomId, rp: this.bot.config.roomPassword, fp: `@${md5(username || this.bot.config.usename)}` }; } (this.loginObj.lr) ? '' : delete this.loginObj.lr; socket.addEventListener('open', async () => { this.bot.loggerInfo('正在登录中...'); try { const loginPack = '*' + JSON.stringify(this.loginObj); await IIROSE_WSsend(this.bot, loginPack); this.event = startEventsServer(this.bot); // 清理旧的心跳定时器(如果存在) if (this.live) { this.live(); this.live = null; } // 设置基础心跳包保活 if (this.bot.config.keepAliveEnable) { this.startHeartbeat(); } } catch (error) { this.bot.loggerError('登录包发送失败:', error); if (socket.readyState === 1) // WebSocket.OPEN { socket.close(); } } }); return socket; } /** * 接受ws通信 */ accept() { this.firstLogin = false; this.loginSuccess = false; this.isReconnecting = false; // 花园登陆报文 if (!this.bot.socket) { this.bot.loggerError('WebSocket connection is not established.'); return; } this.bot.socket.addEventListener('message', async (event) => { const array = new Uint8Array(event.data); let message: string; if (array[0] === 1) { message = zlib.unzipSync(array.slice(1)).toString(); } else { message = Buffer.from(array).toString("utf8"); } if (message.length < 500) { this.bot.fulllogInfo(`[WS接收]`, message); } // 检查是否有等待特定响应的监听器 for (const [prefix, handler] of this.bot.responseListeners.entries()) { if (message.startsWith(prefix)) { handler.listener(message); if (handler.stopPropagation) { return; // 消息已被作为响应处理,停止进一步解码 } } } const currentUsername = this.bot.config.smStart ? this.bot.config.smUsername : this.bot.config.usename; if (message.includes(">") && message.includes(currentUsername)) { const messageIdMatch = message.match(/(\d{12,})$/); if (messageIdMatch) { const messageId = messageIdMatch[1]; const userPattern = new RegExp(`>${currentUsername}>`, "i"); if (userPattern.test(message)) { if (this.bot.messageIdResolvers.length > 0) { const resolver = this.bot.messageIdResolvers.shift(); if (resolver) { resolver(messageId); } } } } } // 检查是否是响应消息 if (message.startsWith('+') || message.startsWith('i!')) { if (this.bot.handleResponse(message)) { return; // 如果消息被作为响应处理,则停止进一步解码 } } if (!this.firstLogin) { this.firstLogin = true; if (message.startsWith(`%*"0`)) { this.bot.loggerError(`登录失败:名字被占用,用户名:${this.loginObj.n}`); this.bot.status = Universal.Status.OFFLINE; await this.bot.stop(); await sleep(1000); this.ctx.scope.dispose(); return; } else if (message.startsWith(`%*"1`)) { this.bot.loggerError("登录失败:用户名不存在"); this.bot.status = Universal.Status.OFFLINE; await this.bot.stop(); await sleep(1000); this.ctx.scope.dispose(); return; } else if (message.startsWith(`%*"2`)) { this.bot.loggerError(`登录失败:密码错误,用户名:${this.loginObj.n}`); this.bot.status = Universal.Status.OFFLINE; await this.bot.stop(); await sleep(1000); this.ctx.scope.dispose(); return; } else if (message.startsWith(`%*"4`)) { this.bot.loggerError(`登录失败:今日可尝试登录次数达到上限,用户名:${this.loginObj.n}。请尝试更换网络后重新登陆。`); this.bot.status = Universal.Status.OFFLINE; await this.bot.stop(); await sleep(1000); this.ctx.scope.dispose(); return; } else if (message.startsWith(`%*"5`)) { this.bot.loggerError(`登录失败:房间密码错误,用户名:${this.loginObj.n},房间id:${this.loginObj.r}`); this.bot.status = Universal.Status.OFFLINE; await this.bot.stop(); await sleep(1000); this.ctx.scope.dispose(); return; } else if (message.startsWith(`%*"x`)) { this.bot.loggerError(`登录失败:用户被封禁,用户名:${this.loginObj.n}`); this.bot.status = Universal.Status.OFFLINE; await this.bot.stop(); await sleep(1000); this.ctx.scope.dispose(); return; } else if (message.startsWith(`%*"n0`)) { this.bot.loggerError(`登录失败:房间无法进入,用户名:${this.loginObj.n},房间id:${this.loginObj.r}`); this.bot.status = Universal.Status.OFFLINE; await this.bot.stop(); await sleep(1000); this.ctx.scope.dispose(); return; } else if (message.startsWith(`%`)) { // 登录成功 this.bot.logInfo(this.loginObj); this.bot.loggerInfo(`[${this.bot.config.uid}] 登陆成功:欢迎回来,${this.loginObj.n}!`); // 设置为在线 this.bot.status = Universal.Status.ONLINE; this.bot.online(); } } const funcObj = await decoder(this.bot, message); // console.log(funcObj) // 将会话上报 if (funcObj.manyMessage) { const reversedMessages = funcObj.manyMessage.slice().reverse(); for (const element of reversedMessages) { if (!element.type) { continue; } const test: Record<string, any> = {}; const type = element.type; // 根据消息类型,正确地准备要传递给 decoderMessage 的数据 // 对于 memberUpdate,我们传递 payload,对于其他消息,我们传递 element 本身 if (type === 'memberUpdate' && element.payload) { test[type] = element.payload; } else { test[type] = element; } await decoderMessage(test, this.bot); } } else { await decoderMessage(funcObj, this.bot); } }); } /** * 开始ws通信 */ async start() { // 检查是否正在停用 if (this.disposed) { return; } // 防止重复启动 if (this.isStarting || this.isStarted) { return; } this.isStarting = true; try { // 如果不是重连状态,设置为连接中 if (this.bot.status !== Universal.Status.RECONNECT) { this.bot.status = Universal.Status.CONNECT; } // 清理旧的连接和定时器 this.cleanup(); this.bot.socket = await this.prepare(); if (!this.bot.socket) { throw new Error('WebSocket连接创建失败'); } this.accept(); this.setupEventListeners(); this.isStarted = true; } catch (error) { // 如果插件正在停用,不记录错误 if (!this.disposed) { this.bot.loggerError('WebSocket启动失败:', error); } // 确保清理状态 this.isStarted = false; // 只有在非停用状态下才重新抛出错误 if (!this.disposed) { throw error; // 重新抛出错误,让上层处理 } } finally { this.isStarting = false; } } /** * 清理连接和定时器 */ private cleanup() { if (this.live) { this.live(); this.live = null; } if (this.reconnectTimer) { this.reconnectTimer(); this.reconnectTimer = null; } if (this.event.length > 0) { stopEventsServer(this.event); this.event = []; } if (this.bot.socket) { // 移除所有事件监听器 this.bot.socket.removeEventListener('open', () => { }); this.bot.socket.removeEventListener('message', () => { }); this.bot.socket.removeEventListener('close', () => { }); this.bot.socket.removeEventListener('error', () => { }); if (this.bot.socket.readyState === 1 || this.bot.socket.readyState === 0) // WebSocket.OPEN || WebSocket.CONNECTING { this.bot.socket.close(); } this.bot.socket = undefined; } } /** * 设置WebSocket事件监听器 */ private setupEventListeners() { if (!this.bot.socket) return; this.bot.socket.addEventListener('error', (error) => { this.bot.loggerError('WebSocket 连接错误:', error); if (!this.disposed) { this.handleConnectionLoss(); } }); this.bot.socket.addEventListener('close', async (event) => { const code = event.code; // 检查是否应该重连 if ( this.bot.status == Universal.Status.RECONNECT || this.bot.status == Universal.Status.DISCONNECT || this.bot.status == Universal.Status.OFFLINE || code == 1000 || this.disposed ) { if (!this.isReconnecting) { this.bot.logInfo("websocket停止:正常关闭,不重连"); } return; } // 如果是重连状态,不输出异常关闭日志 if (this.isReconnecting) { return; } this.bot.loggerWarn(`websocket异常关闭,代码: ${code},将在5秒后重连`); this.handleConnectionLoss(); }); } /** * 启动心跳保活机制 */ private startHeartbeat() { if (this.live) { if (this.live) this.live(); } this.live = this.ctx.setInterval(async () => { if (this.disposed) { return; } if (this.bot.socket) { if (this.bot.socket.readyState === 1) // WebSocket.OPEN { if (this.bot.status == Universal.Status.ONLINE) { // this.bot.fulllogInfo(`发送空包(心跳保活) 实例: ${this.bot.user?.id || 'unknown'}`); try { await IIROSE_WSsend(this.bot, ''); // 心跳包不需要严格的错误处理 } catch (error) { // 心跳发送失败不阻断后续逻辑 this.bot.loggerWarn('心跳包发送失败:', error); } } } else if (this.bot.socket.readyState === 3 || this.bot.socket.readyState === 2) // WebSocket.CLOSED || WebSocket.CLOSING { this.bot.loggerWarn(`心跳保活检测到连接异常 实例: ${this.bot.user?.id || 'unknown'}, readyState: ${this.bot.socket.readyState}`); this.handleConnectionLoss(); } } else { this.bot.loggerWarn(`心跳保活检测到socket为空 实例: ${this.bot.user?.id || 'unknown'}`); this.handleConnectionLoss(); } }, 30 * 1000); // 30秒心跳间隔 } /** * 处理连接丢失,执行重连逻辑 */ private handleConnectionLoss() { if (this.isReconnecting || this.disposed) { return; // 避免重复重连 } this.bot.loggerWarn(`检测到连接丢失,准备重连 实例: ${this.bot.user?.id || 'unknown'}`); this.isReconnecting = true; this.isStarting = false; this.isStarted = false; // 设置机器人状态为重连中 this.bot.status = Universal.Status.RECONNECT; // 清理当前连接 this.cleanup(); // 设置重连定时器 this.reconnectTimer = this.ctx.setTimeout(async () => { if (this.disposed) { this.isReconnecting = false; return; } try { this.bot.loggerInfo(`开始重连 实例: ${this.bot.user?.id || 'unknown'}`); // 设置为连接中状态 this.bot.status = Universal.Status.CONNECT; await this.start(); this.isReconnecting = false; } catch (error) { if (!this.disposed) { this.bot.loggerError(`重连失败 实例: ${this.bot.user?.id || 'unknown'}:`, error); // 如果重连失败,等待更长时间后再次尝试 this.isReconnecting = false; this.ctx.setTimeout(() => { if (!this.disposed) { this.handleConnectionLoss(); } }, 10000); // 10秒后再次尝试 } else { this.isReconnecting = false; } } }, 5000); } /** * 关闭ws通信 */ async stop() { // 立即设置停用状态 this.setDisposing(true); // 只有在真正停止时才设置为离线状态,重连时不设置 if (!this.isReconnecting) { this.bot.status = Universal.Status.DISCONNECT; } // 重置状态 this.isStarting = false; this.isStarted = false; // 清理所有定时器 if (this.live) { this.live(); this.live = null; } if (this.reconnectTimer) { this.reconnectTimer(); this.reconnectTimer = null; } if (this.event.length > 0) { stopEventsServer(this.event); // 停止事件服务器的逻辑 this.event = []; // 清空事件数组 } // 移除事件监听器和关闭连接 if (this.bot.socket) { // 移除所有事件监听器 this.bot.socket.removeEventListener('open', () => { }); this.bot.socket.removeEventListener('message', () => { }); this.bot.socket.removeEventListener('close', () => { }); this.bot.socket.removeEventListener('error', () => { }); // 强制关闭连接 if (this.bot.socket.readyState === 1 || this.bot.socket.readyState === 0) // WebSocket.OPEN || WebSocket.CONNECTING { this.bot.socket.close(1000, 'Plugin disposing'); } this.bot.socket = undefined; } } /** * 获取延迟 * @param url * @returns */ private getLatency(url: string): Promise<number | 'error'> { return new Promise((resolve) => { // 检查是否正在停用 if (this.disposed) { resolve('error'); return; } let ws: WebSocket | null = null; let timeoutId: (() => void) | null = null; let disposingCheckId: (() => void) | null = null; let resolved = false; const cleanup = () => { if (timeoutId) { timeoutId(); timeoutId = null; } if (disposingCheckId) { disposingCheckId(); disposingCheckId = null; } if (ws && (ws.readyState === 1 || ws.readyState === 0)) // WebSocket.OPEN || WebSocket.CONNECTING { try { ws.close(); } catch (e) { // 忽略关闭错误 } } ws = null; }; const safeResolve = (value: number | 'error') => { if (!resolved) { resolved = true; cleanup(); resolve(value); } }; try { const startTime = Date.now(); // 增加超时时间,给网络更多时间 const timeout = Math.max(this.bot.config.timeout, 2000); // 至少2秒 ws = this.ctx.http.ws(url); // 设置超时 timeoutId = this.ctx.setTimeout(() => { safeResolve('error'); }, timeout); // 定期检查停用状态 disposingCheckId = this.ctx.setInterval(() => { if (this.disposed) { safeResolve('error'); } }, 200); ws.addEventListener('open', () => { const endTime = Date.now(); const latency = endTime - startTime; safeResolve(latency); }); ws.addEventListener('error', () => { safeResolve('error'); }); ws.addEventListener('close', () => { if (!resolved) { safeResolve('error'); } }); } catch (error) { safeResolve('error'); } }); } /** * 切换房间 * 重新连接到新房间 */ async switchRoom() { // 保存当前状态 const wasReconnecting = this.isReconnecting; // 设置为重连状态,避免被当作异常断线 this.isReconnecting = true; // 重置状态 this.isStarting = false; this.isStarted = false; // 清理当前连接但不设置 disposed this.cleanup(); try { // 重新连接 await this.start(); } catch (error) { this.bot.loggerError('房间切换失败:', error); throw error; } finally { // 恢复重连状态 this.isReconnecting = wasReconnecting; } } } // WebSocket发送锁,确保消息发送时序正确 let wsSendLock = Promise.resolve(); export async function IIROSE_WSsend(bot: IIROSE_Bot, data: string): Promise<void> { const callId = Math.random().toString(36).substring(2, 8); wsSendLock = wsSendLock.then(async () => { try { if (!bot.socket) { // 布豪! // 牙白! bot.loggerError("布豪! !bot.socket !!! 请联系开发者"); return; } if (bot.socket.readyState == 0) { bot.loggerError("布豪! bot.socket.readyState == 0 !!! 请联系开发者"); return; } const shortData = data.length > 50 ? data.substring(0, 50) + '...' : data; if (shortData.trim().length > 0) { bot.fulllogInfo(`[WS发送-${callId}] 发送数据: ${shortData}`); } else { // 不打印心跳 // bot.fulllogInfo(`[WS发送-${callId}] 发送数据: ${shortData}`); } const buffer = Buffer.from(data); const uintArray: Uint8Array = Uint8Array.from(buffer); if (uintArray.length > 256) { const deflatedData = zlib.gzipSync(data); const deflatedArray: Uint8Array = new Uint8Array(deflatedData.length + 1); deflatedArray[0] = 1; deflatedArray.set(deflatedData, 1); bot.socket.send(deflatedArray); } else { bot.socket.send(uintArray); } } catch (error) { bot.loggerError(`[WS发送-${callId}] 发送失败:`, error); } }); // 等待当前操作完成 await wsSendLock; };