koishi-plugin-adapter-iirose
Version:
[IIROSE-蔷薇花园](https://iirose.com/)适配器
347 lines (299 loc) • 7.75 kB
text/typescript
import { Context } from 'koishi';
import { IIROSE_Bot } from '../../bot/bot';
import { LoginObj } from './types';
import { getMd5Password, md5 } from '../password';
import { calculateRetryDelay, waitWithCancel } from './retry';
import { IIROSE_WSsend } from './send';
/**
* 测试服务器延迟
*/
export async function getLatency(
ctx: Context,
bot: IIROSE_Bot,
url: string,
disposed: () => boolean
): Promise<number | 'error'>
{
return new Promise((resolve) =>
{
if (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))
{
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(bot.config.timeout, 2000);
ws = ctx.http.ws(url);
timeoutId = ctx.setTimeout(() =>
{
safeResolve('error');
}, timeout);
disposingCheckId = ctx.setInterval(() =>
{
if (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');
}
});
}
/**
* 准备 WebSocket 连接,测试服务器并选择最快的
*/
export async function prepareConnection(
ctx: Context,
bot: IIROSE_Bot,
disposed: () => boolean
): Promise<WebSocket>
{
const iiroseList = ['m1', 'm2', 'm8', 'm9', 'm'];
let fastest = 'www';
let maximumSpeed = 100000;
let allErrors: boolean;
let retryCount = 0;
const maxRetryIntervalMinutes = bot.config.maxRetryInterval;
do
{
if (disposed())
{
throw new Error('插件正在停用');
}
allErrors = true;
const speedTests: Promise<{ index: string, speed: number | 'error'; }>[] = [];
// 并行测试所有服务器
for (let webIndex of iiroseList)
{
speedTests.push(
getLatency(ctx, bot, `wss://${webIndex}.iirose.com:8778`, disposed)
.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 =>
ctx.setTimeout(() => resolve(iiroseList.map(index => ({ index, speed: 'error' as const }))), 5000)
)
]);
if (disposed())
{
throw new Error('插件正在停用');
}
// 找到最快的可用服务器
for (const result of results)
{
if (result.speed !== 'error')
{
allErrors = false;
if (maximumSpeed > result.speed)
{
fastest = result.index;
maximumSpeed = result.speed;
}
}
}
if (!allErrors)
{
break;
}
} catch (error)
{
bot.loggerWarn('服务器测试过程中出现错误:', error);
}
if (allErrors)
{
const delayMs = calculateRetryDelay(retryCount, maxRetryIntervalMinutes);
const delaySec = Math.round(delayMs / 1000);
if (!bot.config.silentRetry || bot.config.debugMode)
{
bot.loggerWarn(`所有服务器都无法连接,将在${delaySec}秒后重试... (重试次数: ${retryCount})`);
}
const cancelled = await waitWithCancel(ctx, bot, delayMs, disposed);
if (cancelled)
{
throw new Error('插件正在停用');
}
retryCount++;
if (disposed())
{
throw new Error('插件正在停用');
}
}
} while (allErrors && !disposed());
if (!fastest)
{
fastest = 'www';
}
const targetUrl = `wss://${fastest}.iirose.com:8778`;
bot.loggerInfo(`找到可用服务器: ${targetUrl}, 延迟: ${maximumSpeed}ms`);
const socket = ctx.http.ws(targetUrl);
socket.binaryType = 'arraybuffer';
const dispose = ctx.on('dispose', () =>
{
if (socket && socket.readyState === 1)
{
socket.close();
}
dispose();
});
return socket;
}
/**
* 创建登录对象
*/
export function createLoginObj(bot: IIROSE_Bot): LoginObj
{
const roomIdReg = /\s*\[_([\\s\\S]+)_\]\s*/;
const userNameReg = /\s*\[\\*([\\s\\S]+)\\*\]\s*/;
const roomIdConfig = bot.config.roomId;
const userNameConfig = bot.config.usename;
let username = (userNameReg.test(userNameConfig)) ? userNameConfig.match(userNameReg)?.[1] : userNameConfig;
let room = (roomIdReg.test(roomIdConfig)) ? roomIdConfig.match(roomIdReg)?.[1] : roomIdConfig;
let loginObj: LoginObj;
if (bot.config.smStart && bot.config.smPassword === 'ec3a4ac482b483ac02d26e440aa0a948')
{
loginObj = {
r: bot.config.smRoom,
n: bot.config.smUsername,
i: bot.config.smImage,
nc: bot.config.smColor,
s: bot.config.smGender,
st: bot.config.smst,
mo: bot.config.smmo,
uid: bot.config.smUid,
li: bot.config.smli,
mb: bot.config.smmb,
mu: bot.config.smmu,
la: bot.config.smLocation,
vc: bot.config.smvc,
fp: `@${md5(bot.config.smUsername)}`
};
bot.loggerInfo('已启用蔷薇游客模式');
} else
{
const hashedPassword = getMd5Password(bot.config.password);
if (!hashedPassword)
{
bot.loggerError('登录失败:密码不能为空');
throw new Error('密码不能为空');
}
loginObj = {
r: room || bot.config.roomId,
n: username || bot.config.usename,
p: hashedPassword,
st: bot.config.botStatus,
mo: bot.config.signature,
mb: '',
mu: '01',
lr: bot.config.oldRoomId,
rp: bot.config.roomPassword,
fp: `@${md5(username || bot.config.usename)}`
};
}
if (!loginObj.lr)
{
delete loginObj.lr;
}
return loginObj;
}
/**
* 设置连接打开事件
*/
export function setupOpenEvent(
socket: WebSocket,
bot: IIROSE_Bot,
loginObj: LoginObj,
startHeartbeat: () => void,
startEvents: () => (() => boolean)[]
)
{
socket.addEventListener('open', async () =>
{
bot.loggerInfo('正在登录中...');
try
{
const loginPack = '*' + JSON.stringify(loginObj);
await IIROSE_WSsend(bot, loginPack);
const events = startEvents();
if (bot.config.keepAliveEnable)
{
startHeartbeat();
}
return events;
} catch (error)
{
bot.loggerError('登录包发送失败:', error);
if (socket.readyState === 1)
{
socket.close();
}
throw error;
}
});
}