koishi-plugin-adapter-iirose
Version:
[IIROSE-蔷薇花园](https://iirose.com/)适配器
656 lines (567 loc) • 18.6 kB
text/typescript
import { Context, Bot, Fragment, Universal, Logger, Session } from 'koishi';
import { readJsonData, findRoomInGuild, flattenRooms, findUserNameById, Unknown_User_Name, Unknown_Guild_Name, Unknown_Channel_Name } from '../utils/utils';
import { IIROSE_BotMessageEncoder } from './sendMessage';
import { IIROSE_WSsend, WsClient } from '../utils/ws';
import { Internal, InternalType } from './internal';
import { comparePassword } from '../utils/password';
import { SendOptions } from '@satorijs/protocol';
import { SessionCache } from '../utils/sessionCache';
import kick from '../encoder/admin/manage/kick';
import mute from '../encoder/admin/manage/mute';
import { Stock } from '../decoder/messages/Stock';
import { BankCallback } from '../decoder/messages/BankCallback';
import { Config } from '../config';
export class IIROSE_Bot extends Bot<Context>
{
static MessageEncoder = IIROSE_BotMessageEncoder;
platform: string = 'iirose';
socket: WebSocket | undefined = undefined;
public messageIdResolvers: ((messageId: string) => void)[] = [];
public responseQueue: {
resolver: (data: string | null) => void;
timer: () => void;
}[] = [];
public responseListeners = new Map<string, { listener: (data: string) => void, stopPropagation: boolean; }>();
static inject = ['assets'];
public wsClient: WsClient;
public readonly config: Config;
public sessionCache: SessionCache;
private isStarting: boolean = false;
private isStarted: boolean = false;
private disposed: boolean = false;
private userInfoTimeout: (() => void) | null = null;
private lastStockData: Stock | null = null;
private lastBankData: BankCallback | null = null;
public logger: Logger;
public userLeaveTimers = new Map<string, () => void>();
public userJoinTimers = new Map<string, () => void>();
constructor(public ctx: Context, config: Config)
{
super(ctx, {}, 'iirose-bot');
this.platform = 'iirose';
this.config = config;
this.logger = new Logger(`DEV:adapter-iirose`);
this.sessionCache = new SessionCache(config.sessionCacheSize);
// 重置状态
this.isStarting = false;
this.isStarted = false;
this.disposed = false;
this.userInfoTimeout = null;
this.wsClient = new WsClient(ctx, this);
if (this.config.smStart && comparePassword(this.config.smPassword, 'ec3a4ac482b483ac02d26e440aa0a948d309c822'))
{
this.selfId = this.config.smUid;
this.userId = this.config.smUid;
} else
{
this.selfId = this.config.uid;
this.userId = this.config.uid;
}
}
public loggerError(message: any, ...args: any[]): void
{
this.ctx.logger.error(`[${this.config.uid}]`, message, ...args);
}
public loggerInfo(message: any, ...args: any[]): void
{
this.ctx.logger.info(`[${this.config.uid}]`, message, ...args);
}
public loggerWarn(message: any, ...args: any[]): void
{
this.ctx.logger.warn(`[${this.config.uid}]`, message, ...args);
}
public logInfo(message: any, ...args: any[]): void
{
if (this.config.debugMode)
{
this.logger.info(`[${this.config.uid}]`, message, ...args);
}
}
public fulllogInfo(message: any, ...args: any[]): void
{
if (this.config.fullDebugMode)
{
this.logger.info(`[${this.config.uid}]`, message, ...args);
}
}
setDisposing(disposing: boolean)
{
this.disposed = disposing;
// 将停用状态传递给 WebSocket 客户端
if (this.wsClient && this.wsClient.setDisposing)
{
this.wsClient.setDisposing(disposing);
}
}
async start()
{
// 检查是否正在停用
if (this.disposed)
{
return;
}
// 防止重复启动
if (this.isStarting || this.isStarted)
{
return;
}
this.isStarting = true;
try
{
// 设置为连接中状态
this.status = Universal.Status.CONNECT;
// 启动 WebSocket 连接
await this.wsClient.start();
this.isStarted = true;
} catch (error)
{
// 如果插件正在停用,不记录错误
if (!this.disposed)
{
this.loggerError('机器人启动失败:', error);
} else
{
}
this.isStarted = false;
} finally
{
this.isStarting = false;
}
}
async stop()
{
// 如果已经停止,直接返回
if (this.disposed)
{
return;
}
// 立即设置停用状态
this.setDisposing(true);
// 重置状态
this.isStarting = false;
this.isStarted = false;
// 立即清理定时器
if (this.userInfoTimeout)
{
this.userInfoTimeout();
this.userInfoTimeout = null;
}
// 清理所有用户离开计时器
this.userLeaveTimers.forEach(dispose => dispose());
this.userLeaveTimers.clear();
// 清理所有用户加入计时器
this.userJoinTimers.forEach(dispose => dispose());
this.userJoinTimers.clear();
// 立即下线
this.offline();
const session = this.session({
type: 'login-removed',
platform: this.platform,
selfId: this.selfId,
});
this.dispatch(session);
this.fulllogInfo('login-removed', session);
// 停止 WebSocket 连接
if (this.wsClient)
{
// 使用 Promise.race 限制等待时间
await Promise.race([
this.wsClient.stop(),
new Promise(resolve => this.ctx.setTimeout(() => resolve(undefined), 500)) // 最多等待500ms
]);
}
}
async sendMessage(channelId: string, content: Fragment, guildId?: string, options?: SendOptions): Promise<string[]>
{
if (!channelId)
{
return [];
}
const encoder = new IIROSE_BotMessageEncoder(this, channelId, guildId, options);
const messages = await encoder.send(content);
return messages.map(m => m.id);
}
async sendPrivateMessage(userId: string, content: Fragment, guildId?: string, options?: SendOptions): Promise<string[]>
{
return this.sendMessage(`private:${userId}`, content);
}
online()
{
super.online();
// 派发 login-updated 事件
const session = this.session({
type: 'login-updated',
platform: this.platform,
selfId: this.selfId,
});
this.dispatch(session);
this.fulllogInfo('login-updated', session);
}
async getSelf(): Promise<Universal.User>
{
// 直接调用getUser方法获取自身信息
return this.getUser(this.selfId);
}
/**
* 发送一个WebSocket请求并等待对应的响应
* @param payload 要发送的数据
* @param timeout 超时时间 (毫秒)
* @returns 返回一个Promise,该Promise会解析为响应字符串,或在超时/失败时解析为null
*/
public requestResponse(payload: string, timeout?: number): Promise<string | null>
{
const effectiveTimeout = timeout ?? this.config.timeout;
return new Promise((resolve) =>
{
IIROSE_WSsend(this, payload);
const timer = this.ctx.setTimeout(() =>
{
// 超时,从队列中移除,null
const index = this.responseQueue.findIndex(p => p.timer === timer);
if (index > -1)
{
this.responseQueue.splice(index, 1);
}
resolve(null);
}, effectiveTimeout);
this.responseQueue.push({
resolver: (data) =>
{
timer(); // 取消计时器
resolve(data);
},
timer,
});
});
}
/**
* 处理一个进入的响应,并将其分发到响应队列中的第一个等待者
* @param data 响应数据
* @returns 如果消息被处理,则返回true
*/
public handleResponse(data: string): boolean
{
const request = this.responseQueue.shift();
if (request)
{
request.timer(); // 取消计时器
request.resolver(data);
return true;
}
return false;
}
/**
* 发送一个WebSocket消息并等待一个具有特定前缀的响应
* @param payload 要发送的数据
* @param responsePrefix 期望的响应前缀
* @param stopPropagation 是否在匹配到响应后停止消息的进一步传播,默认为 true
* @param timeout 超时时间 (毫秒)
* @returns 返回一个Promise,该Promise会解析为响应字符串,或在超时时解析为null
*/
public sendAndWaitForResponse(payload: string, responsePrefix: string, stopPropagation: boolean = true, timeout?: number): Promise<string | null>
{
const effectiveTimeout = timeout ?? this.config.timeout;
return new Promise((resolve) =>
{
const dispose = this.ctx.setTimeout(() =>
{
this.responseListeners.delete(responsePrefix);
resolve(null); // 超时,解析为 null
}, effectiveTimeout);
this.responseListeners.set(responsePrefix, {
listener: (data: string) =>
{
dispose(); // 取消计时器
this.responseListeners.delete(responsePrefix); // clean up after resolving
resolve(data);
},
stopPropagation: stopPropagation
});
IIROSE_WSsend(this, payload);
});
}
async getUser(userId: string): Promise<Universal.User>
{
const userlist = await readJsonData(this, 'wsdata/userlist.json');
if (!userlist)
{
return { id: userId, name: Unknown_User_Name };
}
const user = userlist.find(u => u.uid === userId);
if (!user)
{
return { id: userId, name: Unknown_User_Name };
}
return {
id: user.uid,
name: user.username,
avatar: user.avatar,
};
}
async getGuildMember(guildId: string, userId: string): Promise<Universal.GuildMember>
{
const user = await this.getUser(userId);
// 返回基础用户信息
return {
...user,
// roles: [],
};
}
async getGuildMemberList(guildId: string, next?: string): Promise<Universal.List<Universal.GuildMember>>
{
const userlist = await readJsonData(this, 'wsdata/userlist.json');
if (!userlist) return { data: [] };
const members = userlist
.filter(u => u.room === guildId)
.map(u => ({
id: u.uid,
name: u.username,
avatar: u.avatar,
}));
return { data: members };
}
async getGuild(guildId: string): Promise<Universal.Guild>
{
const roomlist = await readJsonData(this, 'wsdata/roomlist.json');
if (!roomlist) return { id: guildId, name: Unknown_Guild_Name };
const guild = findRoomInGuild(roomlist, guildId);
if (!guild) return { id: guildId, name: Unknown_Guild_Name };
return {
id: guild.id,
name: guild.name,
};
}
async getGuildList(next?: string): Promise<Universal.List<Universal.Guild>>
{
// 机器人所在群组列表,只有一个:当前聊天室
const currentRoomId = this.config.roomId;
const guild = await this.getGuild(currentRoomId);
return { data: [guild] };
}
async getFriendList(next?: string): Promise<Universal.List<Universal.User>>
{
// 没有好友的概念
return null;
}
async handleFriendRequest(messageId: string, approve: boolean, comment?: string): Promise<void>
{
// 所有用户都可以直接私聊
}
async getChannel(channelId: string): Promise<Universal.Channel>
{
if (channelId.startsWith('private:'))
{
const userId = channelId.substring(8);
const user = await this.getUser(userId);
return {
id: channelId,
name: user.name,
type: Universal.Channel.Type.DIRECT,
};
}
const roomId = channelId;
const roomlist = await readJsonData(this, 'wsdata/roomlist.json');
if (!roomlist) return { id: roomId, name: Unknown_Channel_Name, type: Universal.Channel.Type.TEXT };
const room = findRoomInGuild(roomlist, roomId);
if (!room) return { id: roomId, name: Unknown_Channel_Name, type: Universal.Channel.Type.TEXT };
return {
id: room.id,
name: room.name,
type: Universal.Channel.Type.TEXT,
};
}
async getChannelList(guildId: string): Promise<Universal.List<Universal.Channel>>
{
const roomlist = await readJsonData(this, 'wsdata/roomlist.json');
if (!roomlist) return { data: [] };
// 查找对应的社区(Guild)
const guildData = findRoomInGuild(roomlist, guildId);
if (!guildData) return { data: [] };
// 将该社区下的所有房间(包括子房间)扁平化
const channels = flattenRooms(guildData).map(room => ({
id: room.id,
name: room.name,
type: Universal.Channel.Type.TEXT,
}));
return { data: channels };
}
async getMessage(channelId: string, messageId: string): Promise<Universal.Message>
{
const session = this.sessionCache.findById(messageId);
if (session)
{
return {
id: session.messageId,
messageId: session.messageId,
content: session.content,
channel: {
id: session.channelId,
type: session.channelId.startsWith('private:') ? Universal.Channel.Type.DIRECT : Universal.Channel.Type.TEXT,
},
guild: session.guildId ? { id: session.guildId } : undefined,
user: session.author,
timestamp: session.timestamp,
quote: session.quote,
};
}
return undefined;
}
getMessageKeys(): string[]
{
return this.sessionCache.getAllMessageIds();
}
/**
* 获取频道消息列表
* @param channelId 频道 ID
* @param next 分页令牌,未指定时视为从最新消息向前获取(本实现中忽略此参数)
* @param direction 消息获取方向,可以为 'before' | 'after' | 'around'
* @returns 消息列表
*/
async getMessageList(channelId: string, next?: string, direction: 'before' | 'after' | 'around' = 'before'): Promise<Universal.List<Universal.Message>>
{
// 从缓存中获取所有消息
const allSessions = this.sessionCache.getAllMessageIds().map(id => this.sessionCache.findById(id)).filter(Boolean) as Session[];
// 过滤出指定频道的消息
const channelSessions = allSessions.filter(session => session.channelId === channelId);
// 根据方向排序消息
let sortedSessions: Session[];
if (direction === 'after')
{
// 按时间正序(从旧到新)
sortedSessions = [...channelSessions].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
} else
{
// 默认按时间倒序(从新到旧)
sortedSessions = [...channelSessions].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
}
// 转换为消息格式
const messages = sortedSessions.map(session => ({
id: session.messageId,
messageId: session.messageId,
content: session.content,
channel: {
id: session.channelId,
type: session.channelId.startsWith('private:') ? Universal.Channel.Type.DIRECT : Universal.Channel.Type.TEXT,
},
guild: session.guildId ? { id: session.guildId } : undefined,
user: {
id: session.userId,
name: session.username,
},
timestamp: session.timestamp,
quote: session.quote,
}));
return { data: messages };
}
async kickGuildMember(guildId: string, userId: string, permanent?: boolean): Promise<void>
{
// 从 userlist.json 获取用户名
const userName = await findUserNameById(this, userId);
// 如果成功获取用户名,则执行踢出操作
if (userName)
{
await IIROSE_WSsend(this, kick(userName));
} else
{
this.loggerWarn(`无法找到用户ID: ${userId} 对应的用户名,无法执行踢出操作。`);
}
}
async muteGuildMember(guildId: string, userId: string, duration: number, reason?: string): Promise<void>
{
// 从 userlist.json 获取用户名
const userName = await findUserNameById(this, userId);
// 如果成功获取用户名,则执行禁言操作
if (userName)
{
let time: string;
// 永久禁言
if ((duration / 1000) > 99999)
{
time = '&';
} else
{
time = String(duration / 1000);
}
if (reason == undefined)
{
reason = '';
}
await IIROSE_WSsend(this, mute('all', userName, time, reason));
} else
{
this.loggerWarn(`无法找到用户ID: ${userId} 对应的用户名,无法执行禁言操作。`);
}
}
async deleteMessage(channelId: string, messageId: string | string[]): Promise<void>
{
try
{
await new Promise(resolve => this.ctx.setTimeout(() => resolve(undefined), this.config.deleteMessageDelay));
// 如果是数组,逐个撤回
if (Array.isArray(messageId))
{
for (const id of messageId)
{
await this.deleteSingleMessage(channelId, id);
}
} else
{
// 单个消息撤回
await this.deleteSingleMessage(channelId, messageId);
}
} catch (error)
{
this.loggerError('删除消息失败:', error);
}
}
// 撤回单个消息
private async deleteSingleMessage(channelId: string, messageId: string): Promise<boolean>
{
try
{
// 根据频道类型确定撤回命令格式
let deleteCommand: string;
if (channelId.startsWith('private:'))
{
const userId = channelId.split(":")[1];
deleteCommand = `v0*${userId}#${messageId}`;
} else
{
deleteCommand = `v0#${messageId}`;
}
this.logInfo(`[撤回消息开始] 频道: ${channelId}, 消息ID: ${messageId}`);
if (this.socket && this.socket.readyState === WebSocket.OPEN)
{
await IIROSE_WSsend(this, deleteCommand);
return true;
} else
{
this.loggerWarn('WebSocket连接未就绪,无法撤回消息');
return false;
}
} catch (error)
{
this.loggerError(`撤回消息失败 (channelId: ${channelId}, messageId: ${messageId}):`, error);
return false;
}
}
internal: InternalType = new Internal(this);
public handleStockUpdate(newStockData: Stock)
{
if (JSON.stringify(this.lastStockData) !== JSON.stringify(newStockData))
{
this.lastStockData = newStockData;
this.logInfo('iirose/stock-update', newStockData);
this.ctx.emit('iirose/stock-update', newStockData);
}
}
public handleBankUpdate(newBankData: BankCallback)
{
if (JSON.stringify(this.lastBankData) !== JSON.stringify(newBankData))
{
this.lastBankData = newBankData;
this.logInfo('iirose/bank-update', newBankData);
this.ctx.emit('iirose/bank-update', newBankData);
}
}
}