mirai-js
Version:
QQ robot development framework based on Mirai-api-http.
1,222 lines (1,071 loc) • 45 kB
JavaScript
// 引入核心功能,前缀下划线时为了与方法名区别 (视觉上的区别)
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 };