UNPKG

mirai-js

Version:

QQ robot development framework based on Mirai-api-http.

1,222 lines (1,071 loc) 45 kB
// 引入核心功能,前缀下划线时为了与方法名区别 (视觉上的区别) const _releaseSession = require('./core/releaseSession'); const _verify = require('./core/auth'); const _bind = require('./core/verify'); const _sendCommand = require('./core/sendCommand'); const _sendFriendMessage = require('./core/sendFriendMessage'); const _sendGroupMessage = require('./core/sendGroupMessage'); const _sendTempMessage = require('./core/sendTempMessage'); const _sendNudge = require('./core/sendNudge'); const _getSessionConfig = require('./core/getSessionConfig'); const _setSessionConfig = require('./core/setSessionConfig'); const _uploadImage = require('./core/uploadImage'); const _uploadVoice = require('./core/uploadVoice'); const _getFriendList = require('./core/getFriendList'); const _getGroupList = require('./core/getGroupList'); const _getMemberList = require('./core/getMemberList'); const _getMemberInfo = require('./core/getMemberInfo'); const _getUserProfile = require('./core/getUserProfile'); const _setMemberInfo = require('./core/setMemberInfo'); const _setMemberAdmin = require('./core/setMemberAdmin'); const _getAnnoList = require('./core/anno/getAnno'); const _publishAnno = require('./core/anno/publishAnno'); const _deleteAnno = require('./core/anno/deleteAnno'); const _recall = require('./core/recall'); const _mute = require('./core/mute'); const _muteAll = require('./core/muteAll'); const _unmute = require('./core/unmute'); const _unmuteAll = require('./core/unmuteAll'); const _removeMember = require('./core/removeMember'); const _removeFriend = require('./core/removeFriend'); const _quitGroup = require('./core/quitGroup'); const _getGroupConfig = require('./core/getGroupConfig'); const _setGroupConfig = require('./core/setGroupConfig'); const _setEssence = require('./core/setEssence'); const _messageFromId = require('./core/messageFromId'); const { wsStartListening: _startListening, wsStopListening: _stopListening } = require('./polyfill/wsListener'); // 其他 const random = require('./util/random')(0, 2E16); const getInvalidParamsString = require('./util/getInvalidParamsString'); const { Waiter } = require('./Waiter'); const { FileManager } = require('./FileManager'); const { errCodeEnum } = require('./util/errCode'); const { isBrowserEnv } = require('./util/isBrowserEnv'); const fs = isBrowserEnv() ? null : require('fs'); const { promisify } = isBrowserEnv() ? { promisify: null } : require('util'); // 扩展接口 const { MessageChainGetable, BotConfigGetable } = require('./interface'); /** * @field config 包含 baseUrl verifyKey qq * @field eventProcessorMap 事件处理器 map * @field wsConnection 建立连接的 WebSocket 实例 * @field waiter 内部类单例,提供同步 io 机制 */ class Bot extends BotConfigGetable { constructor() { super(); this.waiter = new Waiter(this); this.config = undefined; this.eventProcessorMap = {}; this.wsConnection = undefined; } /** * 实现 BotConfigGetable 接口 */ getBaseUrl() { return this.config.baseUrl; } getQQ() { return this.config.qq; } getVerifyKey() { return this.config.verifyKey; } getSessionKey() { return this.config.sessionKey; } /** * @description 连接到 mirai-api-http,并开启一个会话,重复调用意为重建会话 * open 方法 1. 建立会话 2. 绑定 qq 3. 与服务端建立 WebSocket 连接 * @param {string} baseUrl 必选,mirai-api-http server 的地址 * @param {string} verifyKey 必选,mirai-api-http server 设置的 verifyKey * @param {number} qq 必选,欲绑定的 qq 号,需要确保该 qq 号已在 mirai-console 登陆 * @param {boolean} singleMode 可选,mirai-api-http server 是否启用了 singleMode * @returns {void} */ async open({ baseUrl, qq, verifyKey, singleMode } = {}) { // 若 config 存在,则认为该对象已经 open 过 // ,此处应该先令对象回到初始状态,然后重建会话 if (this.config) { await this.close({ keepProcessor: true, keepConfig: true }); } // 设置对象状态 // 若开发者重复调用 open,仅更新已提供的值 this.config = { baseUrl: this.config?.baseUrl ?? baseUrl, qq: this.config?.qq ?? qq, verifyKey: this.config?.verifyKey ?? verifyKey, sessionKey: this.config?.sessionKey ?? '', }; // 事件处理器 map // 如果重复调用 open 则保留事件处理器 this.eventProcessorMap = this.eventProcessorMap ?? {}; // 需要使用的参数 ({ baseUrl, qq, verifyKey } = this.config); // 检查参数 if (!this.config.baseUrl || !this.config.qq || !this.config.verifyKey) { throw new Error(`open 缺少必要的 ${getInvalidParamsString({ baseUrl, qq, verifyKey, })} 参数`); } // 创建会话 const sessionKey = this.config.sessionKey = await _verify({ baseUrl, verifyKey }); // 绑定到一个 qq, 若开启了 singleMode 则需要跳过绑定 !singleMode && await _bind({ baseUrl, sessionKey, qq }); // 配置服务端 websocket 状态 // await _setSessionConfig({ baseUrl, sessionKey, enableWebsocket: true }); // 开始监听事件 await this.__wsListen(); } /** * @private * @description 监听 ws 消息 */ async __wsListen() { const { baseUrl, sessionKey, verifyKey } = this.config; this.wsConnection = await _startListening({ baseUrl, sessionKey, verifyKey, message: data => { // 如果当前到达的事件拥有处理器,则依次调用所有该事件的处理器 if (data.type in this.eventProcessorMap) { data.bot = this; return Object.values(this.eventProcessorMap[data.type]) .forEach(processor => processor(data)); } }, error: err => { const type = 'error'; if (type in this.eventProcessorMap) { err.bot = this; return Object.values(this.eventProcessorMap[type]) .forEach(processor => processor(err)); } try { console.log(`ws error\n${JSON.stringify(err)}`); } catch (error) { } // eslint-disable-line no-empty }, close: (obj) => { const type = 'close'; if (type in this.eventProcessorMap) { obj.bot = this; return Object.values(this.eventProcessorMap[type]) .forEach(processor => processor(obj)); } try { console.log(`ws close\n${JSON.stringify(obj)}`); } catch (error) { }// eslint-disable-line no-empty }, unexpectedResponse: (obj) => { const type = 'unexpected-response'; if (type in this.eventProcessorMap) { obj.bot = this; return Object.values(this.eventProcessorMap[type]) .forEach(processor => processor(obj)); } try { console.log(`ws unexpectedResponse\n${JSON.stringify(obj)}`); } catch (error) { }// eslint-disable-line no-empty } }); } /** * @description 关闭会话 * @param {boolean} keepProcessor 可选,是否保留事件处理器,默认值为 false,不保留 * @param {boolean} keepConfig 可选,是否保留 session baseUrl qq verifyKey,默认值为 false,不保留 * @returns {void} */ async close({ keepProcessor = false, keepConfig = false } = {}) { // 检查对象状态 if (!this.config) { throw new Error('close 请先调用 open,建立一个会话'); } // 需要使用的参数 const { baseUrl, sessionKey, qq } = this.config; // 关闭 ws 连接 await _stopListening(this.wsConnection); // 释放会话 await _releaseSession({ baseUrl, sessionKey, qq }); // 初始化对象状态 if (!keepProcessor) { this.eventProcessorMap = {}; } if (!keepConfig) { this.config = undefined; } this.wsConnection = undefined; } /** * ! messageChain 将在未来被移除 * @description 向 qq 好友 或 qq 群发送消息,若同时提供,则优先向好友发送消息 * @param {boolean} temp 可选,是否是临时会话,默认为 false * @param {number} friend 二选一,好友 qq 号 * @param {number} group 二选一,群号 * @param {number} quote 可选,消息引用,使用发送时返回的 messageId * @param {Message} message 必选,Message 实例或 MessageType 数组 * @returns {number} messageId */ async sendMessage({ temp = false, friend, group, quote, message, messageChain }) { // 检查对象状态 if (!this.config) { throw new Error('sendMessage 请先调用 open,建立一个会话'); } // 检查参数 if (!friend && !group | !message && !messageChain) { throw new Error(`sendMessage 缺少必要的 ${getInvalidParamsString({ 'friend 或 group': friend || group, 'message 或 messageChain': message || messageChain, })} 参数`); } if (messageChain) { console.log('warning: 现在 sendMessage 方法的 message 参数可以同时接收 Message 实例或 messageChain,messageChain 参数将在未来被移除'); } // 需要使用的参数 const { baseUrl, sessionKey } = this.config; // 处理 message,兼容存在 messageChain 参数的版本 messageChain = messageChain ?? message; if (messageChain instanceof MessageChainGetable) { messageChain = messageChain.getMessageChain(); } else if (typeof messageChain === 'string') { messageChain = [{ type: 'Plain', text: messageChain, }]; } // 根据 temp、friend、group 参数的情况依次调用 if (temp) { // 临时会话的接口,好友和群是在一起的,在内部做了参数判断并抛出异常 // 而正常的好友和群的发送消息接口是分开的,所以在外面做了参数判断并抛出异常,格式相同 return await _sendTempMessage({ baseUrl, sessionKey, qq: friend, group, quote, messageChain }); } else { if (friend) { return await _sendFriendMessage({ baseUrl, sessionKey, target: friend, quote, messageChain }); } else if (group) { return await _sendGroupMessage({ baseUrl, sessionKey, target: group, quote, messageChain }); } else { throw { message: 'sendGroupMessage 缺少必要的 qq 或 group 参数' }; } } } /** * @description 向好友或群成员发送戳一戳 * 如果提供了 group 参数则忽略 friend * mirai-api-http-v1.10.1 feature * @param {number} friend 二选一,好友 qq 号 * @param {number} group 二选一,群成员所在群 * @param {number} target 必选,目标 qq 号 */ async sendNudge({ friend, group, target }) { // 检查对象状态 if (!this.config) { throw new Error('sendNudge 请先调用 open,建立一个会话'); } // 检查参数 if (!((group || friend) && target)) { throw new Error(`sendNudge 缺少必要的 ${getInvalidParamsString({ 'group 或 friend': group || friend, 'target': target, })} 参数`); } // 需要使用的参数 const { baseUrl, sessionKey } = this.config; // 发给群成员 if (group) { await _sendNudge({ baseUrl, sessionKey, target, subject: group, kind: 'Group', }); } // 发给好友 else if (friend) { await _sendNudge({ baseUrl, sessionKey, target, subject: friend, kind: 'Friend', }); } } /** * @description 添加一个事件处理器 * 框架维护的 WebSocket 实例会在 ws 的事件 message 下分发 Mirai http server 的消息 * 回调函数 (data) => void,data 的结构取决于消息类型,详见 mirai-api-http 的文档 * 而对于 ws 的其他事件 error, close, unexpectedResponse,其回调函数分别为 * - 'error': (err: Error) => void * - 'close': (code: number, message: string) => void * - 'unexpected-response': (request: http.ClientRequest, response: http.IncomingMessage) => void * @param {string | string[]} eventType 必选,事件类型 * @param {function} callback 必选,回调函数 * @returns {number | string[]} 事件处理器的标识,用于移除该处理器 */ on(eventType, callback) { // 检查对象状态 if (!this.config) { throw new Error('on 请先调用 open,建立一个会话'); } // 检查参数 if (!eventType || !callback) { throw new Error(`on 缺少必要的 ${getInvalidParamsString({ eventType, callback })} 参数`); } // 适配 eventType 数组 if (Array.isArray(eventType)) { return eventType.map(event => this.on(event, callback)); } // 为没有任何事件处理器的事件生成一个空对象 (空对象 {},而不是 null) if (!(eventType in this.eventProcessorMap)) { this.eventProcessorMap[eventType] = {}; } // 生成一个唯一的 handle,作为当前 // processor 的标识,用于移除该处理器 let handle = random(); while (handle in this.eventProcessorMap[eventType]) { handle = random(); } // processor // 每个事件对应多个 processor,这些 processor 和 // handle 分别作为 value 和 key 包含在一个大对象中 let processor = callback; // 添加事件处理器 this.eventProcessorMap[eventType][handle] = processor; return handle; } /** * @description 添加一个一次性事件处理器,回调一次后自动移除 * @param {string | string[]} eventType 必选,事件类型 * @param {function} callback 必选,回调函数 * @param {boolean} strict 可选,是否严格检测调用,由于消息可能会被中间件拦截 * 当为 true 时,只有开发者的处理器结束后才会移除该处理器 * 当为 false 时,即使消息被拦截,也会移除该处理器 * @returns {void} */ one(eventType, callback, strict = false) { // 检查对象状态 if (!this.config) { throw new Error('one 请先调用 open,建立一个会话'); } // 检查参数 if (!eventType || !callback) { throw new Error(`one 缺少必要的 ${getInvalidParamsString({ eventType, callback })} 参数`); } // 适配 eventType 数组 if (Array.isArray(eventType)) { eventType.map(event => this.one(event, callback)); return; } // 为没有任何事件处理器的事件生成一个空对象 (空对象 {},而不是 null) if (!(eventType in this.eventProcessorMap)) { this.eventProcessorMap[eventType] = {}; } // 生成一个唯一的 handle,作为当前 // processor 的标识,用于移除该处理器 let handle = random(); while (handle in this.eventProcessorMap[eventType]) { handle = random(); } // processor // 每个事件对应多个 processor,这些 processor 和 h // andle 分别作为 value 和 key 包含在一个大对象中 const processor = async (data) => { if (strict) { // 严格检测回调 // 当开发者的处理器结束后才移除该处理器,这里等待异步回调 await callback(data); if (handle in this.eventProcessorMap[eventType]) { delete this.eventProcessorMap[eventType][handle]; } } else { // 不严格检测,直接移除处理器 // 从 field eventProcessorMap 中移除 handle 指定的事件处理器 if (handle in this.eventProcessorMap[eventType]) { delete this.eventProcessorMap[eventType][handle]; } // 调用开发者提供的回调 callback(data); } }; // 添加事件处理器 this.eventProcessorMap[eventType][handle] = processor; } /** * @description 移除一个事件处理器 * @param {string} eventType 必选,事件类型 * @param {number | number[]} handle * 可选,事件处理器标识(或数组),由 on 方法返回,未提供时将移除该事件下的所有处理器 * @returns {void} */ off(eventType, handle) { // 检查对象状态 if (!this.config) { throw new Error('off 请先调用 open,建立一个会话'); } // 检查参数 if (!eventType) { throw new Error('off 缺少必要的 eventType 参数'); } if (handle) { // 从 field eventProcessorMap 中移除 handle 指定的事件处理器 if (handle.forEach) { // 可迭代 handle.forEach(hd => { if (hd in this.eventProcessorMap[eventType]) { delete this.eventProcessorMap[eventType][hd]; } }); } else { // 不可迭代,认为是单个标识 if (handle in this.eventProcessorMap[eventType]) { delete this.eventProcessorMap[eventType][handle]; } } } else { // 未提供 handle,移除所有 if (eventType in this.eventProcessorMap) { delete this.eventProcessorMap[eventType]; } } } /** * @description 移除所有事件处理器 * @param {string | string[]} eventType 可选,事件类型(或数组) * @returns {void} */ offAll(eventType) { // 检查对象状态 if (!this.config) { throw new Error('offAll 请先调用 open,建立一个会话'); } if (eventType) { // 提供了特定的 eventType 参数 if (eventType.forEach) { // 可迭代 eventType.forEach(evtType => { if (evtType in this.eventProcessorMap) { delete this.eventProcessorMap[evtType]; } }); } else { // 不可迭代 if (eventType in this.eventProcessorMap) { delete this.eventProcessorMap[eventType]; } } } else { // 未提供参数,全部移除 this.eventProcessorMap = {}; } } /** * @description 获取 config * @returns {Object} 结构 { cacheSize, enableWebsocket } */ async getSessionConfig() { // 检查对象状态 if (!this.config) { throw new Error('getConfig 请先调用 open,建立一个会话'); } const { baseUrl, sessionKey } = this.config; return await _getSessionConfig({ baseUrl, sessionKey }); } /** * @description 设置 config * @param {number} cacheSize 可选,插件缓存大小 * @param {boolean} enableWebsocket 可选,websocket 状态 * @returns {void} */ async setSessionConfig({ cacheSize, enableWebsocket }) { // 检查对象状态 if (!this.config) { throw new Error('setConfig 请先调用 open,建立一个会话'); } const { baseUrl, sessionKey } = this.config; await _setSessionConfig({ baseUrl, sessionKey, cacheSize, enableWebsocket }); } /** * @description 撤回由 messageId 确定的消息 * @param {number} messageId 欲撤回消息的 messageId * @param {number} target 目标群/ qq 号 * @returns {void} */ async recall({ messageId, target }) { // 检查对象状态 if (!this.config) { throw new Error('recall 请先调用 open,建立一个会话'); } // 检查参数 if (!messageId) { // target 参数在 mirai-api-http v2.6.0 后变更 throw new Error('recall 缺少必要的 messageId 参数'); } const { baseUrl, sessionKey } = this.config; // 撤回消息 if (target === undefined) { // 兼容 mirai-api-http v2.6.0 前的接口 await _recall({ baseUrl, sessionKey, target: messageId }); } else { // mirai-api-http v2.6.0+ await _recall({ baseUrl, sessionKey, messageId, target }); } } /** * FIXME: type 指定为 'friend' 或 'temp' 时发送的图片显示红色感叹号,无法加载,group 则正常 * @description 上传图片至服务器,返回指定 type 的 imageId,url,及 path * @param {string} type 可选,"friend" 或 "group" 或 "temp",默认为 "group" * @param {Buffer} img 二选一,图片二进制数据 * @param {string} filename 二选一,图片文件路径 * @returns {Object} 结构 { imageId, url, path } */ async uploadImage({ type = 'group', img, filename }) { if (isBrowserEnv()) { throw new Error('uploadImage 在浏览器环境下不可用'); } // 检查对象状态 if (!this.config) { throw new Error('uploadImage 请先调用 open,建立一个会话'); } // 检查参数 if (isBrowserEnv() && filename) { throw new Error('uploadImage 浏览器端不支持 filename 参数'); } if (!img && !filename) { throw new Error('uploadImage 缺少必要的 img 或 filename 参数'); } // 若传入 filename 则统一转换为 Buffer if (filename) { // 优先使用 img 的原值 img = img ?? await promisify(fs.readFile)(filename); } const { baseUrl, sessionKey } = this.config; return await _uploadImage({ baseUrl, sessionKey, type, img }); } /** * FIXME: 目前该功能返回的 voiceId 无法正常使用,无法 * 发送给好友,提示 message is empty,发到群里则是 1s 的无声语音 * @description 上传语音至服务器,返回 voiceId, url 及 path * @param {string} type TODO: 目前仅支持 "group",请忽略该参数 * @param {Buffer} voice 二选一,语音二进制数据 * @param {string} filename 二选一,语音文件路径 * @returns {Object} 结构 { voiceId, url, path } */ async uploadVoice({ type = 'group', voice, filename }) { if (isBrowserEnv()) { throw new Error('uploadVoice 在浏览器环境下不可用'); } // 检查对象状态 if (!this.config) { throw new Error('uploadVoice 请先调用 open,建立一个会话'); } // 检查参数 if (isBrowserEnv() && filename) { throw new Error('uploadVoice 浏览器端不支持 filename 参数'); } if (!voice && !filename) { throw new Error('uploadVoice 缺少必要的 voice 或 filename 参数'); } // 若传入 filename 则统一转换为 Buffer if (filename) { // 优先使用 img 的原值 voice = voice ?? await promisify(fs.readFile)(filename); } const { baseUrl, sessionKey } = this.config; return await _uploadVoice({ baseUrl, sessionKey, type, voice }); } /** * @description 获取好友列表 * @returns {Object[]} 结构 array[...{ id, name, remark }] */ async getFriendList() { // 检查对象状态 if (!this.config) { throw new Error('getFriendList 请先调用 open,建立一个会话'); } const { baseUrl, sessionKey } = this.config; // 获取列表 const friendList = await _getFriendList({ baseUrl, sessionKey }); // 这里希望所有列表应该具有相同的属性名 friendList.map((value) => { value.name = value.nickname; delete value.nickname; }); return friendList; } /** * @description 获取群列表 * @returns {Object[]} 结构 array[...{ id, name, permission }] */ async getGroupList() { // 检查对象状态 if (!this.config) { throw new Error('getGroupList 请先调用 open,建立一个会话'); } const { baseUrl, sessionKey } = this.config; return await _getGroupList({ baseUrl, sessionKey }); } /** * @description 获取指定群的成员列表 * @param {number} group 必选,欲获取成员列表的群号 * @returns {Object[]} 结构 array[...{ id, name, permission }] */ async getMemberList({ group }) { // 检查对象状态 if (!this.config) { throw new Error('getMemberList 请先调用 open,建立一个会话'); } // 检查参数 if (!group) { throw new Error('getMemberList 缺少必要的 group 参数'); } // 获取列表 const { baseUrl, sessionKey } = this.config; const memberList = await _getMemberList({ baseUrl, sessionKey, target: group }); // 这里希望所有列表应该具有相同的属性名 memberList.map((value) => { value.name = value.memberName; delete value.group; delete value.memberName; }); return memberList; } /** * @description 获取群成员信息 * @param {number} group 必选,群成员所在群号 * @param {number} qq 必选,群成员的 qq 号 * @returns {Object} */ async getMemberInfo({ group, qq }) { // 检查对象状态 if (!this.config) { throw new Error('getMemberInfo 请先调用 open,建立一个会话'); } // 检查参数 if (!group || !qq) { throw new Error(`getMemberInfo 缺少必要的 ${getInvalidParamsString({ group, qq })} 参数`); } // 获取列表 const { baseUrl, sessionKey } = this.config; const memberInfo = await _getMemberInfo({ baseUrl, sessionKey, target: group, memberId: qq }); // 将 specialTitle 改为 title,在 setMemberInfo 也保持一致 memberInfo.title = memberInfo.specialTitle; delete memberInfo.specialTitle; return memberInfo; } /** * @description 获取群成员信息 * @param {number} qq 必选,用户的 qq 号 * @returns {Object} 结构 { nickname, email, age, level, sign, sex } */ async getUserProfile({ qq }) { // 检查对象状态 if (!this.config) { throw new Error('getUserProfile 请先调用 open,建立一个会话'); } // 检查参数 if (!qq) { throw new Error('getUserProfile 缺少必要的 qq 参数'); } const { baseUrl, sessionKey } = this.config; return await _getUserProfile({ baseUrl, sessionKey, target: qq }); } /** * @description 设置群成员信息 * @param {number} group 必选,群成员所在群号 * @param {number} qq 必选,群成员的 qq 号 * @param {string} name 可选,要设置的群名片 * @param {string} title 可选,要设置的群头衔 * @param {boolean} permission 可选,要设置的权限, * 使用枚举值:Bot.Permission.Admin, Bot.Permission.Member * @returns {void} */ async setMemberInfo({ group, qq, name, title, permission }) { // 检查对象状态 if (!this.config) { throw new Error('setMemberInfo 请先调用 open,建立一个会话'); } // 检查参数 if (!group || !qq) { throw new Error(`setMemberInfo 缺少必要的 ${getInvalidParamsString({ group, qq })} 参数`); } if (permission != undefined && permission != Bot.groupPermission.ADMINISTRATOR && permission != Bot.groupPermission.MEMBER) { throw new Error('setMemberInfo admin 参数只能是 Bot.groupPermission.ADMINISTRATOR 或 Bot.groupPermission.Member'); } // setMemberInfo const { baseUrl, sessionKey } = this.config; if (name != undefined || title != undefined) { await _setMemberInfo({ baseUrl, sessionKey, target: group, memberId: qq, name, specialTitle: title, }); } // setPermission if (permission != undefined) { await _setMemberAdmin({ baseUrl, sessionKey, target: group, memberId: qq, assign: permission == Bot.groupPermission.ADMINISTRATOR ? true : false, }); } } /** * @description 获取群公告列表迭代器 * @param {number} group 必选,群号 * @returns 迭代器 */ async *getAnnoIter({ group }) { // 检查对象状态 if (!this.config) { throw new Error('getAnno 请先调用 open,建立一个会话'); } // 检查参数 if (!group) { throw new Error('getAnno 缺少必要的 group 参数'); } // 获取列表 const { baseUrl, sessionKey } = this.config; let offset = 0; let annoList = await _getAnnoList({ baseUrl, sessionKey, id: group, offset, size: 10 }); while (annoList.length > 0) { for (const anno of annoList) { yield anno; } // 获取下一页 offset += 10; annoList = await _getAnnoList({ baseUrl, sessionKey, id: group, offset, size: 10 }); } return; } /** * @description 发布群公告 * @param {number} group 必选,群号 * @param {string} content 必选,公告内容 * @returns {void} */ async publishAnno({ group, content, pinned }) { // 检查对象状态 if (!this.config) { throw new Error('publishAllo 请先调用 open,建立一个会话'); } // 检查参数 if (!group || !content) { throw new Error(`publishAllo 缺少必要的 ${getInvalidParamsString({ group, content })} 参数`); } // 发布公告 const { baseUrl, sessionKey } = this.config; await _publishAnno({ baseUrl, sessionKey, target: group, content, pinned }); } /** * @description 删除群公告 * @param {number} group 必选,群号 * @param {string} fid 必选,公告 id * @reaturns {void} */ async deleteAnno({ group, fid }) { // 检查对象状态 if (!this.config) { throw new Error('deleteAnno 请先调用 open,建立一个会话'); } // 检查参数 if (!group || !fid) { throw new Error(`deleteAnno 缺少必要的 ${getInvalidParamsString({ group, fid })} 参数`); } // 发布公告 const { baseUrl, sessionKey } = this.config; await _deleteAnno({ baseUrl, sessionKey, id: group, fid }); } /** * @description 禁言群成员 * @param {number} group 必选,欲禁言成员所在群号 * @param {number} qq 必选,欲禁言成员 qq 号 * @param {number} time 必选,禁言时长,单位: s (秒) * @returns {void} */ async mute({ group, qq, time }) { // 检查对象状态 if (!this.config) { throw new Error('mute 请先调用 open,建立一个会话'); } // 检查参数 if (!group || !qq || !time) { throw new Error(`mute 缺少必要的 ${getInvalidParamsString({ group, qq, time })} 参数`); } const { baseUrl, sessionKey } = this.config; // 禁言 await _mute({ baseUrl, sessionKey, target: group, memberId: qq, time }); } /** * @description 全员禁言 * @param {number} group 必选,欲全员禁言的群号 * @returns {void} */ async muteAll({ group }) { // 检查对象状态 if (!this.config) { throw new Error('muteAll 请先调用 open,建立一个会话'); } // 检查参数 if (!group) { throw new Error('muteAll 缺少必要的 group 参数'); } const { baseUrl, sessionKey } = this.config; // 禁言 await _muteAll({ baseUrl, sessionKey, target: group }); } /** * @description 解除禁言 * @param {number} group 必选,欲解除禁言的成员所在群号 * @param {number} qq 必选,欲解除禁言的成员 qq 号 * @returns {void} */ async unmute({ group, qq }) { // 检查对象状态 if (!this.config) { throw new Error('unmute 请先调用 open,建立一个会话'); } // 检查参数 if (!group || !qq) { throw new Error(`unmute 缺少必要的 ${getInvalidParamsString({ group, qq })} 参数`); } const { baseUrl, sessionKey } = this.config; // 禁言 await _unmute({ baseUrl, sessionKey, target: group, memberId: qq }); } /** * @description 解除全员禁言 * @param {number} group 必选,欲解除全员禁言的群号 * @returns {void} */ async unmuteAll({ group }) { // 检查对象状态 if (!this.config) { throw new Error('unmute 请先调用 open,建立一个会话'); } // 检查参数 if (!group) { throw new Error('unmute 缺少必要的 group 参数'); } const { baseUrl, sessionKey } = this.config; // 禁言 await _unmuteAll({ baseUrl, sessionKey, target: group }); } /** * @description 移除群成员 * @param {number} group 必选,欲移除的成员所在群号 * @param {number} qq 必选,欲移除的成员 qq 号 * @param {string} message 可选,默认为空串 "",信息 * @returns {void} */ async removeMember({ group, qq, message = '' }) { // 检查对象状态 if (!this.config) { throw new Error('removeMember 请先调用 open,建立一个会话'); } // 检查参数 if (!group || !qq) { throw new Error(`removeMember 缺少必要的 ${getInvalidParamsString({ group, qq })} 参数`); } const { baseUrl, sessionKey } = this.config; // 禁言 await _removeMember({ baseUrl, sessionKey, target: group, memberId: qq, msg: message }); } /** * @description 删除好友 * @param {*} qq 欲删除的好友 qq 号 * @returns {void} */ async removeFriend({ qq }) { // 检查对象状态 if (!this.config) { throw new Error('removeFriend 请先调用 open,建立一个会话'); } // 检查参数 if (!qq) { throw new Error('removeFriend 缺少必要的 qq 参数'); } const { baseUrl, sessionKey } = this.config; // 删除好友 await _removeFriend({ baseUrl, sessionKey, target: qq }); } /** * @description 移除群成员 * @param {number} group 必选,欲移除的成员所在群号 * @returns {void} */ async quitGroup({ group }) { // 检查对象状态 if (!this.config) { throw new Error('quitGroup 请先调用 open,建立一个会话'); } // 检查参数 if (!group) { throw new Error('quitGroup 缺少必要的 group 参数'); } const { baseUrl, sessionKey } = this.config; // 禁言 await _quitGroup({ baseUrl, sessionKey, target: group }); } /** * @description 获取群配置 * @param {number} group 必选,群号 * @returns {Object} */ async getGroupConfig({ group }) { // 检查对象状态 if (!this.config) { throw new Error('getGroupConfig 请先调用 open,建立一个会话'); } // 检查参数 if (!group) { throw new Error('getGroupConfig 缺少必要的 group 参数'); } const { baseUrl, sessionKey } = this.config; return await _getGroupConfig({ baseUrl, sessionKey, target: group }); } /** * @description 设置群配置 * @param {number} group 必选,群号 * @param {string} name 可选,群名 * @param {string} announcement 可选,群公告 * @param {boolean} confessTalk 可选,是否开启坦白说 * @param {boolean} allowMemberInvite 可选,是否允许群员邀请 * @param {boolean} autoApprove 可选,是否开启自动审批入群 * @param {boolean} anonymousChat 可选,是否允许匿名聊天 * @returns {void} */ async setGroupConfig({ group, name, announcement, confessTalk, allowMemberInvite, autoApprove, anonymousChat, }) { // 检查对象状态 if (!this.config) { throw new Error('setGroupConfig 请先调用 open,建立一个会话'); } // 检查参数 if (!group) { throw new Error('setGroupConfig 缺少必要的 group 参数'); } const { baseUrl, sessionKey } = this.config; await _setGroupConfig({ baseUrl, sessionKey, target: group, name, announcement, confessTalk, allowMemberInvite, autoApprove, anonymousChat, }); } /** * @description 文件管理器的工厂方法 * @param {number} group 群号 * @returns {FileManager} 文件管理器实例 */ fileManager({ group }) { return new FileManager({ bot: this, group }); } /** * @description 设置群精华消息 * @param {number} messageId 必选,消息 id * @param {number} target 可选(mahv2.6+),目标群号 * @returns {void} */ async setEssence({ messageId, target }) { // 检查对象状态 if (!this.config) { throw new Error('setEssence 请先调用 open,建立一个会话'); } // 检查参数 if (!messageId) { throw new Error('setEssence 缺少必要的 messageId 参数'); } const { baseUrl, sessionKey } = this.config; if (target === undefined) { // 兼容 mirai-api-http v2.6.0 前的接口 await _setEssence({ baseUrl, sessionKey, target: messageId }); } else { // mirai-api-http v2.6.0+ await _setEssence({ baseUrl, sessionKey, target, messageId }); } } /** * @description 向 mirai-console 发送指令 * @param {string[]} command 必选,指令和参数 * @returns {Object} 结构 { message },注意查看 message 的内容,已知的问题: * 'Login failed: Mirai 无法完成滑块验证. 使用协议 ANDROID_PHONE 强制要求滑块验证, * 请更换协议后重试. 另请参阅: https://github.com/project-mirai/mirai-login-solver-selenium' */ async sendCommand({ command }) { // 检查对象状态 if (!this.config) { throw new Error('setEssence 请先调用 open,建立一个会话'); } // 检查参数 if (!command) { throw new Error(`sendCommand 缺少必要的 ${getInvalidParamsString({ command })} 参数`); } const { Message } = require('./Message'); const { baseUrl, sessionKey } = this.config; return await _sendCommand({ baseUrl, sessionKey, command: command .map((v) => v?.toString() ?? '') .reduce((acc, cur) => acc.addText(cur), new Message) .messageChain }); } /** * @description 通过 messageId 获取消息 * @param {number} target 可选, 目标 qq 号/群号, mah v2.6.0+ 新增该参数 * @param {number} messageId 必选, 消息 id * @returns {Object} 结构 { type, messageChain, sender } */ async getMessageById({ messageId, target }) { // 检查对象状态 if (!this.config) { throw new Error('getMessageById 请先调用 open,建立一个会话'); } // 检查参数 if (!messageId) { throw new Error('getMessageById 缺少必要的 messageId 参数'); } const { baseUrl, sessionKey } = this.config; return await _messageFromId({ baseUrl, sessionKey, target, messageId }); } /** * @description 检测该账号是否已经在 mirai-console 登录 * @param {string} baseUrl 必选,mirai-api-http server 的地址 * @param {string} verifyKey 必选,mirai-api-http server 设置的 verifyKey * @param {number} qq 必选,qq 号 * @returns */ static async isBotLoggedIn({ baseUrl, verifyKey, qq }) { // 检查参数 if (!baseUrl || !verifyKey || !qq) { throw new Error(`isBotLoggedIn 缺少必要的 ${getInvalidParamsString({ baseUrl, verifyKey, qq })} 参数`); } const sessionKey = await _verify({ baseUrl, verifyKey }); const { code } = await _bind({ baseUrl, sessionKey, qq, throwable: false }); if (code == errCodeEnum.BOT_NOT_FOUND) { return false; } else { _releaseSession({ baseUrl, sessionKey, qq }); return true; } } } // 静态属性: 群成员的权限 Bot.groupPermission = { get OWNER() { return 'OWNER'; }, get ADMINISTRATOR() { return 'ADMINISTRATOR'; }, get MEMBER() { return 'MEMBER'; }, }; module.exports = { Bot };