koishi-plugin-adapter-iirose
Version:
[IIROSE-蔷薇花园](https://iirose.com/)适配器
1,038 lines (911 loc) • 27.4 kB
text/typescript
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;
};