import { App, Bot, segment, Session, sleep } from 'koishi'; import 'koishi-adapter-onebot'; import { parseCmd, view } from './command'; import { getLogger } from './loggers'; const logger = getLogger('qqbot'); interface IQQProps { access_token: string; host: string; port: number; bot_id: number; list(chat: IChat, args: string[], replyfn: (msg: string) => any): void; sub(chat: IChat, args: string[], replyfn: (msg: string) => any): void; unsub(chat: IChat, args: string[], replyfn: (msg: string) => any): void; } const cqUrlFix = (factory: segment.Factory) => (...args: Parameters) => factory(...args).replace(/(?<=\[CQ:.*)url=(?=(base64|file|https?):\/\/)/, 'file='); export const Message = { Image: cqUrlFix(segment.image), Video: cqUrlFix(segment.video), Voice: cqUrlFix(segment.audio), ellipseBase64: (msg: string) => msg.replace(/(?<=\[CQ:.*base64:\/\/).*?(,|\])/g, '...$1'), separateAttachment: (msg: string) => { const attachments: string[] = []; const message = msg.replace(/\[CQ:(video|record),.*?\]/g, code => { attachments.push(code); return ''; }); return {message, attachments}; }, }; export default class { private botInfo: IQQProps; private app: App; public bot: Bot; private messageQueues: {[key: string]: (() => Promise)[]} = {}; private next = (type: 'private' | 'group', id: string) => { const queue = this.messageQueues[`${type}:${id}`]; if (queue && queue.length) { queue[0]().then(() => { queue.shift(); if (!queue.length) delete this.messageQueues[`${type}:${id}`]; else this.next(type, id); }); } }; private enqueue = (type: 'private' | 'group', id: string, resolver: () => Promise) => { let wasEmpty = false; const queue = this.messageQueues[`${type}:${id}`] ||= (() => { wasEmpty = true; return []; })(); queue.push(() => sleep(200).then(resolver)); logger.debug(`no. of message currently queued for ${type}:${id}: ${queue.length}`); if (wasEmpty) this.next(type, id); }; private getChat = async (session: Session): Promise => { switch (session.subtype) { case 'private': if (session.groupId) { // temp message const friendList = await session.bot.getFriendList(); if (!friendList.some(friendItem => friendItem.userId === session.userId)) { return { chatID: { qq: Number(session.userId), group: Number(session.groupId), }, chatType: ChatType.Temp, }; } } return { // already befriended chatID: Number(session.userId), chatType: ChatType.Private, }; case 'group': return { chatID: Number(session.groupId), chatType: ChatType.Group, }; } }; private sendToGroup = (groupID: string, message: string) => new Promise(resolve => { this.enqueue('group', groupID, () => this.bot.sendMessage(groupID, message).then(resolve)); }); private sendToUser = (userID: string, message: string) => new Promise(resolve => { this.enqueue('private', userID, () => this.bot.sendPrivateMessage(userID, message).then(resolve)); }); public sendTo = (subscriber: IChat, messageChain: string) => Promise.all( (splitted => [splitted.message, ...splitted.attachments])( Message.separateAttachment(messageChain) ).map(msg => { switch (subscriber.chatType) { case 'group': return this.sendToGroup(subscriber.chatID.toString(), msg); case 'private': return this.sendToUser(subscriber.chatID.toString(), msg); case 'temp': // currently unable to open session, awaiting OneBot v12 return this.sendToUser(subscriber.chatID.qq.toString(), msg); } })) .then(response => { if (response === undefined) return; logger.info(`pushing data to ${JSON.stringify(subscriber.chatID)} was successful, response: ${response}`); }) .catch(reason => { reason = Message.ellipseBase64(reason); logger.error(`error pushing data to ${JSON.stringify(subscriber.chatID)}, reason: ${reason}`); throw Error(reason); }); private initBot = () => { this.app = new App({ type: 'onebot', server: `ws://${this.botInfo.host}:${this.botInfo.port}`, selfId: this.botInfo.bot_id.toString(), token: this.botInfo.access_token, axiosConfig: { maxContentLength: Infinity, }, processMessage: msg => msg.trim(), }); this.app.on('friend-request', async session => { const userString = `${session.username}(${session.userId})`; const groupString = `${session.groupName}(${session.groupId})`; logger.debug(`detected new friend request event: ${userString}`); return session.bot.getGroupList().then(groupList => { if (groupList.some(groupItem => groupItem.groupId === session.groupId)) { session.bot.handleFriendRequest(session.messageId, true); return logger.info(`accepted friend request from ${userString} (from group ${groupString})`); } logger.warn(`received friend request from ${userString} (from group ${groupString})`); logger.warn('please manually accept this friend request'); }); }); this.app.on('group-request', async session => { const userString = `${session.username}(${session.userId})`; const groupString = `${session.groupName}(${session.groupId})`; logger.debug(`detected group invitation event: ${groupString}}`); return session.bot.getFriendList().then(friendList => { if (friendList.some(friendItem => friendItem.userId = session.userId)) { session.bot.handleGroupRequest(session.messageId, true); return logger.info(`accepted group invitation from ${userString} (friend)`); } logger.warn(`received group invitation from ${userString} (stranger)`); logger.warn('please manually accept this group invitation'); }); }); this.app.middleware(async session => { const chat = await this.getChat(session); const cmdObj = parseCmd(session.content); const reply = async msg => session.sendQueued(msg); switch (cmdObj.cmd) { case 'igstory_view': case 'igstory_get': view(chat, cmdObj.args, reply); break; case 'igstory_sub': case 'igstory_subscribe': this.botInfo.sub(chat, cmdObj.args, reply); break; case 'igstory_unsub': case 'igstory_unsubscribe': this.botInfo.unsub(chat, cmdObj.args, reply); break; case 'ping': case 'igstory': this.botInfo.list(chat, cmdObj.args, reply); break; case 'help': if (cmdObj.args.length === 0) { reply(`Instagram 故事搬运机器人: /igstory - 查询当前聊天中的 Instagram Stories 动态订阅 /igstory_subscribe〈链接|用户名〉- 订阅 Instagram Stories 搬运 /igstory_unsubscribe〈链接|用户名〉- 退订 Instagram Stories 媒体搬运 /igstory_view〈链接〉- 查看该用户所有 Stories ${chat.chatType === ChatType.Temp ? '\n(当前游客模式下无法使用订阅功能,请先添加本账号为好友。)' : '' }`); } } }, true); }; private listen = async (logMsg = 'connecting to bot provider...'): Promise => { logger.warn(logMsg); try { await this.app.start(); } catch (err) { logger.error(`error connecting to bot provider at ${this.app.options.server}, will retry in 2.5s...`); await sleep(2500); await this.listen('retry connecting...'); } }; public connect = async () => { this.initBot(); await this.listen(); this.bot = this.app.getBot('onebot'); }; constructor(opt: IQQProps) { logger.warn(`Initialized koishi on ${opt.host}:${opt.port} with access_token ${opt.access_token}`); this.botInfo = opt; } }