const util = require('util');
/**
* @type { import('./src/typedef').WebSocket }
*/
const WebSocket = require('ws');
const semver = require('semver');
const { default: axios } = require('axios');
const Signal = require('./src/utils/Signal');
const checkMAHVersion = require('./src/utils/checkMAHVersion');
const MessageComponent = require('./src/MessageComponent');
const Target = require('./src/target');
const events = require('./src/events.json');
const { Plain } = MessageComponent;
const init = require('./src/init');
const verify = require('./src/verify');
const release = require('./src/release');
const fetchMessage = require('./src/fetchMessage');
const recall = require('./src/recall');
const {
sendFriendMessage,
sendGroupMessage,
sendTempMessage,
sendQuotedFriendMessage,
sendQuotedGroupMessage,
sendQuotedTempMessage,
uploadImage,
uploadVoice,
sendImageMessage,
sendVoiceMessage,
sendFlashImageMessage,
sendNudge,
} = require('./src/sendMessage');
const ws = require('./src/ws');
const {
getFriendList,
getGroupList,
getBotProfile,
getFriendProfile,
getMemberProfile,
getMessageById,
registerCommand,
sendCommand,
getManagers,
botInvitedJoinGroupRequestHandler,
quitGroup,
handleNewFriendRequest,
deleteFriend,
getRoamingMessages,
} = require('./src/manage');
const group = require('./src/group');
const {
uploadFileAndSend,
getGroupFileList,
getGroupFileInfo,
renameGroupFile,
moveGroupFile,
deleteGroupFile,
makeDir,
} = require('./src/fileUtility');
/**
* @typedef MessageResponse
* @property { number } code
* @property { string } msg
* @property { number } messageId
* @property { () => Promise<httpApiResponse> } recall
*
* @typedef { Promise<MessageResponse> } RecallableMessage
*/
/**
* @typedef { import('./src/typedef').Buffer } Buffer
* @typedef { import('./src/typedef').ReadStream } ReadStream
* @typedef { import('./src/typedef').httpApiResponse } httpApiResponse
* @typedef { import('./src/typedef').MessageChain } MessageChain
* @typedef { import('./src/typedef').message } message
* @typedef { import('./src/typedef').UserInfo } UserInfo
* @typedef { import('./src/typedef').GroupMember } GroupMember
* @typedef { import('./src/typedef').Friend } Friend
* @typedef { import('./src/typedef').GroupPermissionInfo } GroupPermissionInfo
* @typedef { import('./src/typedef').GroupInfo } GroupInfo
* @typedef { import('./src/typedef').GroupFile } GroupFile
* @typedef { import('./src/target').MessageTarget } MessageTarget
* @typedef { import('./src/target').GroupTarget } GroupTarget
* @typedef { import('./src/events').EventMap } EventMap
* @typedef { import('./src/events').AllEventMap } AllEventMap
*/
/**
* @namespace NodeMirai
*/
class NodeMirai {
static MessageComponent = MessageComponent
static Target = Target
/**
* @typedef { Object } BotConfig
* @property { string } host http-api 服务的地址
* @property { string } verifyKey http-api 服务的verifyKey
* @property { number } qq bot 的 qq 号
* @property { boolean } [enableWebsocket] 使用 ws 来获取消息和事件推送
* @property { boolean } [wsOnly] 完全使用 ws 来收发消息,为 true 时覆盖 enableWebsocket 且无需调用 verify
* @property { number } [syncId] wsOnly 模式下用于标记 server 主动推送的消息
* @property { number } [interval] 拉取消息的周期(ms), 默认为200
* @property { string } [authKey] (Deprecated) http-api 1.x 版本的authKey
*/
/**
* Create a NodeMirai bot
* @constructor
* @param { BotConfig } config bot config
* @param { string } config.host http-api 服务的地址
* @param { string } config.verifyKey http-api 服务的verifyKey
* @param { number } config.qq bot 的 qq 号
* @param { boolean } [config.enableWebsocket] 使用 ws 来获取消息和事件推送
* @param { boolean } [config.wsOnly] 完全使用 ws 来收发消息,为 true 时覆盖 enableWebsocket 且无需调用 verify
* @param { number } [config.syncId] wsOnly 模式下用于标记 server 主动推送的消息
* @param { number } [config.interval] 拉取消息的周期(ms), 默认为200
* @param { string } [config.authKey] (Deprecated) http-api 1.x 版本的authKey
*/
constructor ({
host,
verifyKey,
qq,
enableWebsocket = false,
wsOnly = false,
syncId = -1,
interval = 200,
authKey,
}) {
this.host = host;
// support 1.x authKey
this.verifyKey = verifyKey || authKey;
this.qq = qq;
this.interval = interval;
this.signal = new Signal();
this.eventListeners = {
message: [],
};
for (let event in events) {
this.eventListeners[events[event]] = [];
}
/**
* @type { string[] }
*/
this.types = [];
this.wsOnly = wsOnly;
this.syncId = syncId;
this.enableWebsocket = wsOnly || enableWebsocket;
/**
* @type { WebSocket | null }
*/
this.wsHost = null;
this.plugins = [];
this._is_mah_v1_ = false;
this.mahVersion = '2.0.0';
checkMAHVersion(this).then(isV1 => {
this._is_mah_v1_ = isV1;
this.auth();
});
}
/**
* @method getBotList
* @returns { number[] }
*/
async getBotList () {
if (semver.lt(this.mahVersion, '2.6.0')) throw new Error('The getBotList API requires mah version >= 2.6.0');
if (this.wsOnly) return ws.send({ command: 'botList' });
return axios.get(`${this.host}/botList`).then(({ data }) => {
if (data.code === 0) return data.data;
return data;
});
}
/**
* @method auth
* @description Bot 认证, 获取 sessionKey
* @returns { Promise<httpApiResponse> }
*/
async auth () {
if (this.enableWebsocket && !this._is_mah_v1_) {
this.wsHost = new WebSocket(`${this.host.replace('http', 'ws')}/all?verifyKey=${this.verifyKey}&qq=${this.qq}`);
if (this.wsOnly) {
// skip binding sessionKey
this.signal.trigger('authed');
this.signal.trigger('verified');
this.startListeningEvents();
ws.init(this.wsHost, this.syncId, this);
return {
code: 0,
msg: 'authed',
};
}
}
return init(this.host, this.verifyKey, this._is_mah_v1_).then(data => {
const { code, session } = data;
if (code !== 0) {
console.error('Failed @ auth: Invalid auth key');
// process.exit(1);
return { code, session };
}
/**
* @type { string }
*/
this.sessionKey = session;
this.signal.trigger('authed');
this.startListeningEvents();
return { code, session };
}).catch((code) => {
console.error('init error with code', code);
// console.error('Failed @ auth: Invalid host');
// process.exit(1);
return {
code: 2,
msg: 'Invalid host',
};
});
}
/**
* @method verify
* @description 校验 sessionKey, 必须在 authed 事件后进行
* @returns { Promise<httpApiResponse> }
*/
async verify () {
if (this.wsOnly) return;
return verify(this.host, this.sessionKey, this.qq, this._is_mah_v1_).then(({ code, msg }) => {
if (code !== 0) {
console.error('Failed @ verify: Invalid session key');
// process.exit(1);
return { code, msg };
}
this.signal.trigger('verified');
return { code, msg };
});
}
/**
* @method release
* @description 释放 sessionKey
* @returns { Promise<httpApiResponse> }
*/
async release () {
return release(this.host, this.sessionKey, this.qq).then(({ code, msg }) => {
if (code !== 0) {
console.error('Failed @ release: Invalid session key');
return { code, msg };
}
this.signal.trigger('released');
return { code, msg };
});
}
/**
* @method fetchMessage
* @param { number } count
* @returns { Promise<message[]> }
*/
async fetchMessage (count = 10) {
return fetchMessage(this.host, this.sessionKey, count).catch(e => {
console.error('Unknown error @ fetchMessage:', e.message);
return [];
// process.exit(1);
});
}
/**
* @method getRoamingMessages
* @description 获取漫游消息
* @param { number } timeStart 起始时间, UTC+8 时间戳, 单位为秒. 可以为 0, 即表示从可以获取的最早的消息起. 负数将会被看是 0
* @param { number } timeEnd 结束时间, 低于 `timeStart` 的值将会被看作是 `timeStart` 的值. 最大支持 `Number.MAX_SAFE_INTEGER`
* @param { number } target 好友 qq, 目前仅支持好友消息漫游
* @returns { message[] }
*/
async getRoamingMessages (timeStart, timeEnd, target) {
return getRoamingMessages({
sessionKey: this.sessionKey,
host: this.host,
timeStart,
timeEnd,
target,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#sendFriendMessage
* @description 发送好友消息
* @param { string | MessageChain[] } messageChain MessageChain 数组
* @param { number } target 发送对象的 qq 号
* @returns { RecallableMessage }
*/
async sendFriendMessage (messageChain, target) {
return sendFriendMessage({ messageChain, target }, this);
}
/**
* @method NodeMirai#sendGroupMessage
* @description 发送群组消息
* @param { string | MessageChain[] } messageChain MessageChain 数组
* @param { number } group 发送群组的群号
* @returns { RecallableMessage }
*/
async sendGroupMessage (messageChain, group) {
return sendGroupMessage({
messageChain: messageChain,
target: group,
}, this);
}
/**
* @method NodeMirai#sendTempMessage
* @description 发送临时消息
* @param { string | MessageChain[] } messageChain MessageChain 数组
* @param { number } qq 临时消息发送对象 QQ 号
* @param { number } group 所在群号
* @returns { RecallableMessage }
*/
async sendTempMessage (messageChain, qq, group) {
// 兼容旧格式:高 32 位为群号,低 32 位为 QQ 号
if (!group)
return sendTempMessage({
messageChain: messageChain,
qq: (qq & 0xFFFFFFFF),
group: ((qq >> 32) & 0xFFFFFFFF),
}, this);
else
return sendTempMessage({
messageChain: messageChain,
qq,
group,
}, this);
}
/**
* @method NodeMirai#sendImageMessage
* @param { string | Buffer | ReadStream } url 图片所在路径
* @param { message | MessageTarget } target 发送目标对象
* @returns { RecallableMessage }
*/
async sendImageMessage (url, target) {
switch (target.type) {
case 'FriendMessage':
return sendImageMessage({
url,
qq: target.sender.id,
}, this);
case 'GroupMessage':
return sendImageMessage({
url,
group: target.sender.group.id,
}, this);
default:
console.error('Error @ sendImageMessage: unknown target type');
}
}
/**
* @method NodeMirai#sendVoiceMessage
* @param { string | Buffer | ReadStream } url 语音所在路径
* @param { GroupTarget } target 发送目标对象(目前仅支持群组)
* @returns { RecallableMessage }
*/
async sendVoiceMessage (url, target) {
if (target.type !== 'GroupMessage')
console.error('Error @ sendVoiceMessage: only support send voice to group');
return sendVoiceMessage({
url,
group: target.sender.group.id,
}, this);
}
/**
* @method NodeMirai#sendFlashImageMessage
* @param { string | Buffer | ReadStream } url 图片所在路径
* @param { message | MessageTarget } target 发送目标对象
* @returns { RecallableMessage }
*/
async sendFlashImageMessage (url, target) {
switch (target.type) {
case 'FriendMessage':
return sendFlashImageMessage({
url,
qq: target.sender.id,
}, this);
case 'GroupMessage':
return sendFlashImageMessage({
url,
group: target.sender.group.id,
}, this);
default:
console.error('Error @ sendFlashImageMessage: unknown target type');
}
}
/**
* @method NodeMirai#uploadImage
* @param { string | Buffer | ReadStream } url 图片所在路径
* @param { message | MessageTarget } target 发送目标对象
* @returns {Promise<{
* imageId: string,
* url: string
* }>}
*/
async uploadImage (url, target) {
let type;
switch (target.type) {
case 'FriendMessage':
type = 'friend';
break;
case 'GroupMessage':
type = 'group';
break;
case 'TempMessage':
type = 'temp';
break;
default:
console.error('Error @ uploadImage: unknown target type');
}
return uploadImage({
url,
type,
}, this);
}
/**
* @method NodeMirai#uploadVoice
* @param { string | Buffer | ReadStream } url 声音所在路径
* @returns {Promise<{
* voiceId: string,
* url: string
* }>}
*/
async uploadVoice (url) {
return uploadVoice({
url,
type: 'group',
}, this);
}
/**
* @method NodeMirai#sendMessage
* @description 发送消息给指定好友或群组
* @param { MessageChain[]|string } message 要发送的消息
* @param { message | MessageTarget } target 发送目标对象
* @returns { RecallableMessage }
*/
async sendMessage (message, target) {
switch (target.type) {
case 'FriendMessage':
return this.sendFriendMessage(message, target.sender.id);
case 'GroupMessage':
return this.sendGroupMessage(message, target.sender.group.id);
case 'TempMessage':
return this.sendTempMessage(message, target.sender.id, target.sender.group.id);
default:
console.error('Invalid target @ sendMessage');
}
}
/**
* @method NodeMirai#sendQuotedFriendMessage
* @description 发送带引用的好友消息
* @param { MessageChain[] } message MessageChain 数组
* @param { number } target 发送对象的 qq 号
* @param { number } quote 引用的 Message 的 id
* @returns { RecallableMessage }
*/
async sendQuotedFriendMessage (message, target, quote) {
return sendQuotedFriendMessage({
messageChain: message,
target,
quote,
}, this);
}
/**
* @method NodeMirai#sendQuotedGroupMessage
* @description 发送带引用的群组消息
* @param { MessageChain[] } message MessageChain 数组
* @param { number } qq 发送群组的群号
* @param { number} quote 引用的 Message 的 id
* @returns { RecallableMessage }
*/
async sendQuotedGroupMessage (message, target, quote) {
return sendQuotedGroupMessage({
messageChain: message,
target, quote,
}, this);
}
/**
* @method NodeMirai#sendQuotedTempMessage
* @description 发送带引用的临时消息
* @param { MessageChain[] } message MessageChain 数组
* @param { number } qq 临时消息发送对象 QQ 号
* @param { number } group 所在群号
* @param { number} quote 引用的 Message 的 id
* @returns { RecallableMessage }
*/
async sendQuotedTempMessage (message, qq, group, quote) {
// 兼容旧格式:高 32 位为群号,低 32 位为 QQ 号
// 若使用旧 API 格式,则 group 位置的值实为 quote
if (!quote)
return sendQuotedTempMessage({
messageChain: message,
qq: (qq & 0xFFFFFFFF),
group: ((qq >> 32) & 0xFFFFFFFF),
quote: group,
sessionKey: this.sessionKey,
host: this.host,
}, this);
else
return sendQuotedTempMessage({
messageChain: message,
qq,
group,
quote,
sessionKey: this.sessionKey,
host: this.host,
}, this);
}
/**
* @method NodeMirai#sendQuotedMessage
* @description 发送引用消息
* @param { MessageChain[]|string } message 要发送的消息
* @param { message } target 发送目标对象
* @returns { RecallableMessage }
*/
async sendQuotedMessage (message, target) {
let quote = target.messageChain[0].type === 'Source' ? target.messageChain[0].id : undefined;
// messageId 可以是负数
if (quote === undefined) throw new Error('Cannot get messageId from target');
// console.log(target.type, quote);
switch (target.type) {
case 'FriendMessage':
return await this.sendQuotedFriendMessage(message, target.sender.id, quote);
case 'GroupMessage':
return await this.sendQuotedGroupMessage(message, target.sender.group.id, quote);
case 'TempMessage':
return await this.sendQuotedTempMessage(message, target.sender.id, target.sender.group.id, quote);
default:
console.error('Invalid target @ sendQuotedMessage');
// process.exit(1);
}
}
/**
* @method NodeMirai#sendNudge
* @description 发送戳一戳消息, 未提供群号时为好友消息
* @param { number } qq 好友或群员的QQ
* @param { number } group 群号
*/
async sendNudge (qq, group) {
if (group) return sendNudge(Object.assign({}, this, { target: qq, subject: group, kind: 'Group' }))
else {
// TODO: Stranger is not supported. Expect returns an error if `qq` is not bot's friend
return sendNudge(Object.assign({}, this, { target: qq, subject: qq, kind: 'Friend' }));
}
}
/**
* @method NodeMirai#reply
* @description 回复一条消息, sendMessage 的别名方法
* @param { MessageChain[]|string } replyMsg 回复的内容
* @param { message | MessageTarget } srcMsg 源消息
* @param { boolean } [quote] 是否引用源消息
* @returns { RecallableMessage }
*/
reply (replyMsg, srcMsg, quote = false) {
const replyMessage = typeof replyMsg === 'string' ? [Plain(replyMsg)] : replyMsg;
if (quote) return this.sendQuotedMessage(replyMessage, srcMsg);
return this.sendMessage(replyMessage, srcMsg);
}
/**
* @method NodeMirai#quoteReply
* @description 引用回复一条消息, sendQuotedMessage 的别名方法
* @param { MessageChain[]|string } replyMsg 回复的内容
* @param { message | MessageTarget } srcMsg 源消息
* @returns { RecallableMessage }
*/
quoteReply (replyMsg, srcMsg) {
const replyMessage = typeof replyMsg === 'string' ? [Plain(replyMsg)] : replyMsg;
return this.sendQuotedMessage(replyMessage, srcMsg);
}
/**
* @method NodeMirai#recall
* @description 撤回一条消息
* @param { message|number } msg 要撤回的消息或消息 id
* @param { number } [targetId] (mah >= v2.6.0)撤回消息为消息 id 时, 需提供好友 qq 或群号
* @returns { Promise<httpApiResponse> }
*/
async recall (msg, targetId) {
let messageId = msg
if (msg.messageId) messageId = msg.messageId
if (msg.messageChain && msg.messageChain[0] && msg.messageChain[0].id) {
messageId = msg.messageChain[0].id
}
if (typeof messageId !== 'number') throw new Error(`Cannot get messageId from ${msg}`)
/**
* API changed since mah v2.6.0
* @see https://github.com/project-mirai/mirai-api-http/releases/tag/v2.6.0
* @see https://github.com/project-mirai/mirai-api-http/commit/8ba96b5dd7362ef83bde9f210d43d319319bc896
*/
if (semver.gte(this.mahVersion, '2.6.0')) {
const target = msg.sender
? msg.sender.group
? msg.sender.group.id
: msg.sender.id
: targetId
if (typeof target !== 'number') {
throw new Error(`Cannot get targetId. Recall by messageId must also pass targetId since mah 2.6.0`)
}
return recall({
target,
messageId,
sessionKey: this.sessionKey,
host: this.host,
wsOnly: this.wsOnly
});
} else {
return recall({
target: messageId,
sessionKey: this.sessionKey,
host: this.host,
wsOnly: this.wsOnly,
});
}
}
/**
* @method NodeMirai#getFriendList
* @description 获取 bot 的好友列表
* @returns { Promise<Friend[]> }
*/
getFriendList () {
return getFriendList({
host: this.host,
sessionKey: this.sessionKey,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#getGroupList
* @description 获取 bot 的群组列表
* @returns { Promise<GroupPermissionInfo[]> }
*/
getGroupList () {
return getGroupList({
host: this.host,
sessionKey: this.sessionKey,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#getBotProfile
* @description 获取 bot 资料
* @returns { Promise<UserInfo> }
*/
getBotProfile () {
return getBotProfile(this);
}
/**
* @method NodeMirai#getFriendProfile
* @param { number } qq 好友的 QQ 号
* @returns { Promise<UserInfo> }
*/
getFriendProfile (qq) {
return getFriendProfile(Object.assign({}, this, { qq }));
}
/**
* @method NodeMirai#getGroupMemberProfile
* @description 获取群员的个人资料
* @param { number } group 群号
* @param { number } qq 群员的 QQ 号
* @returns { Promise<UserInfo> }
*/
getGroupMemberProfile (group, qq) {
return getMemberProfile(Object.assign({}, this, { group, qq }));
}
/**
* @method NodeMirai#getMessageById
* @description 根据消息 id 获取消息内容
* @param { number } messageId 指定的消息 id
* @param { number } target 好友 QQ 或群号
* @return { Promise<{ code: 0|5, data: message }> }
*/
getMessageById (messageId, target) {
return getMessageById(Object.assign({}, this, { messageId, target }));
}
/**
* @method NodeMirai#getGroupMemberList
* @description 获取指定群的成员名单
* @param { number } target 指定的群号
* @returns { Promise<GroupMember[]> }
*/
getGroupMemberList (target) {
return group.getMemberList(Object.assign({}, this, { target }));
}
/**
* @method NodeMirai#setGroupMute
* @description 禁言一位群员(需有相应权限)
* @param { number } target 群号
* @param { number } memberId 群员的 qq 号
* @param { number } time 禁言时间(秒)
* @returns { Promise<httpApiResponse> }
*/
setGroupMute (target, memberId, time = 600) {
return group.setMute(Object.assign({}, this, {
target, memberId, time,
}));
}
/**
* @method NodeMirai#setGroupUnmute
* @description 解除一位群员的禁言状态
* @param { number } target 群号
* @param { number } memberId 群员的 qq 号
* @returns { Promise<httpApiResponse> }
*/
setGroupUnmute (target, memberId) {
return group.setUnmute(Object.assign({}, this, {
target, memberId,
}));
}
/**
* @method NodeMirai#setGroupMuteAll
* @description 设置全体禁言
* @param { number } target 群号
* @returns { Promise<httpApiResponse> }
*/
setGroupMuteAll (target) {
return group.setMuteAll(Object.assign({}, this, { target }));
}
/**
* @method NodeMirai#setGroupUnmuteAll
* @description 解除全体禁言
* @param { number } target 群号
* @returns { Promise<httpApiResponse> }
*/
setGroupUnmuteAll (target) {
return group.setUnmuteAll(Object.assign({}, this, { target }));
}
/**
* @method NodeMirai#setGroupKick
* @description 移除群成员
* @param { number } target 群号
* @param { number } memberId 群员的 qq 号
* @param { string } msg 信息
* @returns { Promise<httpApiResponse> }
*/
setGroupKick (target, memberId, msg = '您已被移出群聊') {
return group.setKick(Object.assign({}, this, {
target, memberId, msg,
}));
}
/**
* @method NodeMirai#setGroupConfig
* @description 修改群设置
* @param { number } target 群号
* @param { Partial<GroupInfo> } config 设置
* @returns { Promise<httpApiResponse> }
*/
setGroupConfig (target, config) {
return group.setConfig(Object.assign({}, this, {
target, config,
}));
}
/**
* @method NodeMirai#setEssence
* @description 设置群精华消息
* @param { number | string | GroupTarget } target 要设置的群
* @param { number } id 精华消息 ID
* @returns { Promise<httpApiResponse> }
*/
setEssence(target, id) {
const { host, sessionKey } = this;
const realTarget = (typeof target === 'number') || (typeof target === 'string')
? target
: target.sender.group.id;
return group.setEssence({
target: realTarget,
id,
host,
sessionKey,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#getGroupConfig
* @description 获取群设置
* @param { number } target 群号
* @returns { Promise<GroupInfo> }
*/
getGroupConfig (target) {
return group.getConfig(Object.assign({}, this, target));
}
/**
* @method NodeMirai#setGroupMemberInfo
* @description 设置群成员信息
* @param { number } target 群号
* @param { number } memberId 群员 qq 号
* @param { Partial<GroupMember> } info 信息
* @returns { Promise<httpApiResponse> }
*/
setGroupMemberInfo (target, memberId, info) {
return group.setMemberInfo(Object.assign({}, this, {
target, memberId, info,
}));
}
/**
* @method NodeMirai#getGroupMemberInfo
* @description 获取群成员信息
* @param { number } target 群号
* @param { number } memberId 群员 qq 号
* @returns { Promise<GroupMember> }
*/
getGroupMemberInfo (target, memberId) {
return group.getMemberInfo(Object.assign({}, this, {
target, memberId,
}));
}
/**
* @method NodeMirai#quit
* @description BOT 主动离群
* @param { number } target 要离开的群的群号
* @returns { Promise<httpApiResponse> }
*/
quit(target) {
return quitGroup(Object.assign({}, this, { target }));
}
/**
* @method NodeMirai#handleMemberJoinRequest
* @description 处理用户入群申请
* @param { number } eventId 入群事件 (memberJoinRequest) ID
* @param { number } fromId 申请入群人 QQ 号
* @param { number } groupId 申请入群群号
* @param { 0|1|2|3|4 } operate 响应操作,0同意,1拒绝,2忽略,3拒绝并拉黑,4忽略并拉黑
* @param { string } message 回复的消息
* @returns { Promise<httpApiResponse> }
*/
handleMemberJoinRequest (eventId, fromId, groupId, operate, message = "") {
return group.handleMemberJoinRequest({
eventId,
fromId,
groupId,
operate,
message,
host: this.host,
sessionKey: this.sessionKey,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#handleBotInvitedJoinGroupRequest
* @description 处理 BOT 被邀请入群的申请
* @param { number } eventId 被邀请入群事件 (botInvitedJoinGroupRequest) ID
* @param { number } fromId 邀请人群者的 QQ 号
* @param { number } groupId 被邀请进入群的群号
* @param { 0|1 } operate 响应的操作类型, 0同意邀请,1拒绝邀请
* @param { string } message 回复的信息
* @returns { Promise<httpApiResponse> }
*/
handleBotInvitedJoinGroupRequest(eventId, fromId, groupId, operate, message = "") {
// 由于方法是单独引入的,所以使用 [event]Handler 而不是 handle[Event] 作为函数名
return botInvitedJoinGroupRequestHandler({
eventId,
fromId,
groupId,
operate,
message,
host: this.host,
sessionKey: this.sessionKey,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#handleNewFriendRequest
* @description 处理好友申请
* @param { number } eventId 好友申请事件 (newFriendRequest) ID
* @param { number } fromId 申请人 QQ 号
* @param { number } groupId 申请人如果通过某个群添加好友,该项为该群群号;否则为0
* @param { 0|1|2 } operate 响应操作,0同意,1拒绝,2拒绝并拉黑
* @param { string } message 回复的消息
* @returns { Promise<httpApiResponse> }
*/
handleNewFriendRequest (eventId, fromId, groupId, operate, message = "") {
return handleNewFriendRequest({
eventId,
fromId,
groupId,
operate,
message,
host: this.host,
sessionKey: this.sessionKey,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#uploadFileAndSend
* @description 上传(群)文件并发送
* @param { string | Buffer | ReadStream } url 文件所在路径或 URL
* @param { string | GroupFile } path 文件要上传到群文件中的位置(路径)
* @param { number | GroupTarget } [target] 要发送文件的目标
* @returns { Promise<httpApiResponse> }
*/
uploadFileAndSend(url, path, target) {
const { sessionKey, host } = this;
if (!target && typeof path === 'object') {
target = path.contact.id;
}
const realTarget = (typeof target === 'number') || (typeof target === 'string')
? target
: target.sender.group.id;
return uploadFileAndSend({
url,
path,
target: realTarget,
sessionKey,
host,
isV1: this._is_mah_v1_,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#getGroupFileList
* @description 获取群文件指定路径下的文件列表
* @param { GroupFile | string | number } dir - - `GroupFile|string` 要获取的群文件路径对象, 使用 `string` 结果可能不准确
* - `number` 获取指定群的群文件根目录 `bot.getGroupFileList(groupId)`
* @param { number | string | GroupTarget } [target] 要获取的群号, `dir` 为 `File` 时可不提供
* @param { boolean } [withDownloadInfo] 是否携带下载信息, 无必要不要携带
* @returns { Promise<GroupFile[]> }
*
*/
getGroupFileList(dir, target, withDownloadInfo) {
const { sessionKey, host } = this;
if (typeof dir === 'object') {
if (!target) target = dir.contact.id;
if (dir.isFile) console.warn(`Warning: Getting list of a file will get empty returns`);
}
// 兼容写法: getGroupFileList(groupid) 返回指定群的根目录
if (typeof dir === 'number' || typeof dir === 'bigint') {
[dir, target] = ['/', dir];
}
const realTarget = (typeof target === 'number') || (typeof target === 'string')
? target
: target.sender.group.id;
return getGroupFileList({
target: realTarget,
dir,
sessionKey,
host,
withDownloadInfo,
isV1: this._is_mah_v1_,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#getGroupFileInfo
* @description 获取群文件指定详细信息
* @param { string | GroupFile } id 文件唯一 ID 或文件对象
* @param { number | string | GroupTarget } [target] 要获取的群号
* @param { boolean } [withDownloadInfo] 是否携带下载信息, 无必要不要携带
* @returns { Promise<GroupFile> }
*/
getGroupFileInfo(id, target, withDownloadInfo) {
const { sessionKey, host } = this;
if (!target && typeof id === 'object') {
target = id.contact.id;
}
const realTarget = (typeof target === 'number') || (typeof target === 'string')
? target
: target.sender.group.id;
return getGroupFileInfo({
target: realTarget,
id,
sessionKey,
host,
withDownloadInfo,
isV1: this._is_mah_v1_,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#renameGroupFile
* @description 重命名指定群文件
* @param { string | GroupFile } id 要重命名的文件唯一 ID 或文件对象
* @param { string } rename 文件的新名称
* @param { number | string | GroupTarget } [target] 目标群号
* @returns { Promise<httpApiResponse> }
*/
renameGroupFile(id, rename, target) {
const { sessionKey, host } = this;
if (!target && typeof id === 'object') {
target = id.contact.id;
}
const realTarget = (typeof target === 'number') || (typeof target === 'string')
? target
: target.sender.group.id;
return renameGroupFile({
target: realTarget,
id,
rename,
sessionKey,
host,
isV1: this._is_mah_v1_,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#moveGroupFile
* @description 移动指定群文件
* @param { string | GroupFile } id 要移动的文件唯一 ID 或文件对象
* @param { string | GroupFile } moveTo 文件的新路径或文件夹对象, 使用 `string` 可能结果不准确
* @param { number | string | GroupTarget } [target] 目标群号
* @returns { Promise<httpApiResponse> }
*/
moveGroupFile(id, moveTo, target) {
const { sessionKey, host } = this;
if (!target && typeof moveTo === 'object') {
target = moveTo.contact.id;
}
const realTarget = (typeof target === 'number') || (typeof target === 'string')
? target
: target.sender.group.id;
return moveGroupFile({
target: realTarget,
id,
moveTo,
sessionKey,
host,
isV1: this._is_mah_v1_,
wsOnly: this.wsOnly,
});
}
/**
* @typedef { httpApiResponse & { data: GroupFile } } makeDirResponse
*/
/**
* @method NodeMirai#makeDir
* @description 创建文件夹
* @param { string | GroupFile | null } id 父目录id, 空串或null为根目录
* @param { string } directoryName 新建文件夹名
* @param { string | number } [target] 群号
* @returns { Promise<makeDirResponse> }
*/
makeDir (id, directoryName, target) {
const { sessionKey, host } = this;
if (!target && typeof id === 'object' && id !== null) {
target = id.contact.id;
}
if (!target) {
console.warn(`Error: Expect providing a target if id is empty`);
}
return makeDir({
sessionKey,
host,
id,
target,
directoryName,
isV1: this._is_mah_v1_,
wsOnly: this.wsOnly,
});
}
/**
* 删除指定群文件
* @param { string | GroupFile } id 要删除的文件唯一 ID
* @param { number | string | GroupTarget } [target] 目标群号
* @returns { Promise<httpApiResponse> }
*/
deleteGroupFile(id, target) {
const { sessionKey, host } = this;
if (!target && typeof id === 'object') {
target = id.contact.id;
}
const realTarget = (typeof target === 'number') || (typeof target === 'string')
? target
: target.sender.group.id;
return deleteGroupFile({
target: realTarget,
id,
sessionKey,
host,
isV1: this._is_mah_v1_,
wsOnly: this.wsOnly,
});
}
/**
* @method NodeMirai#deleteFriend
* @description 删除好友
* @param { number } qq 好友的QQ号
* @returns { Promise<httpApiResponse> }
*/
deleteFriend (qq) {
return deleteFriend(Object.assign({}, this, { target: qq }));
}
getManagers () {
return getManagers({
host: this.host,
verifyKey: this.verifyKey,
qq: this.qq,
});
}
getManager () {
return util.deprecate(this.getManagers, 'NodeMirai#getManager is deprecated, use getManagers instead');
}
// command
/**
* @method NodeMirai#registerCommand
* @param { Object } command 注册的 command 对象
* @param { string } command.name
* @param { string[] } command.alias
* @param { string } command.description
* @param { string } command.usage
* @returns { Promise<httpApiResponse> }
*/
registerCommand (command) {
return registerCommand(Object.assign({
host: this.host,
verifyKey: this.verifyKey,
}, command));
}
/**
* @method NodeMirai#sendCommand
* @param { Object } command 发送的 command 对象
* @param { string } command.name
* @param { string[] } command.args
* @returns { Promise<httpApiResponse> }
*/
sendCommand (command) {
return sendCommand(Object.assign({
host: this.host,
verifyKey: this.verifyKey,
}, command));
}
// event listener
/**
* @method NodeMirai#on
* @description 事件监听
* @template { keyof AllEventMap } N
* @param { N } name
* @param { (message: AllEventMap[N], self?: NodeMirai) => void } callback
*/
on (name, callback) {
if (name === 'message') return this.onMessage(callback);
else if (name === 'command') return this.onCommand(callback);
else if (this.signal.signalList.includes(name)) return this.onSignal(name, callback);
return this.onEvent(name, callback);
}
/**
* @method NodeMirai#onSignal
* @description 订阅 authed, verified, 或 released 信号
* @param { "authed"|"verified"|"released" } signalName 信号
* @param { () => void } callback 回调
*/
onSignal (signalName, callback) {
return this.signal.on(signalName, callback);
}
/**
* @method NodeMirai#onMessage
* @description 订阅消息事件
* @param { (message: message, self?: NodeMirai) => void } callback 回调
*/
onMessage (callback) {
this.eventListeners.message.push(callback);
}
/**
* @template { keyof EventMap } E
* @method NodeMirai#onEvent
* @param { E } event
* @param { (event: EventMap[E], self?: NodeMirai) => void } callback
*/
onEvent (event, callback) {
if (!this.eventListeners[event]) this.eventListeners[event] = [];
this.eventListeners[event].push(callback);
}
onCommand (callback) {
const ws = new WebSocket(`${this.host.replace('http', 'ws')}/command?verifyKey=${this.verifyKey}`);
ws.on('message', message => {
callback(JSON.parse(message));
});
}
/**
* @method NodeMirai#listen
* @description 启动事件监听
* @param { ["all"] | Array<"friend"|"group"|"temp"> } types 类型
*/
listen (...types) {
this.types = [];
if (types.includes('all')) {
this.types.push('FriendMessage', 'GroupMessage', 'TempMessage');
return;
}
for (const type of types) {
switch (type) {
case 'group': this.types.push('GroupMessage'); break;
case 'friend': this.types.push('FriendMessage'); break;
case 'temp': this.types.push('TempMessage'); break;
default:
console.error('Invalid listen type. Type should be "all", "friend", "group" or "temp"');
}
}
}
startListeningEvents () {
if (this.isEventListeningStarted) return;
this.isEventListeningStarted = true;
if (this.wsOnly) return;
if (this.enableWebsocket) {
this.onSignal('verified', () => {
if (!this.wsHost) {
const wsHost = `${this.host.replace('http', 'ws')}/all?sessionKey=${this.sessionKey}`;
this.wsHost = new WebSocket(wsHost);
}
this.wsHost.on('message', message => {
this.emitEventListener(JSON.parse(message));
});
});
}
else setInterval(async () => {
const messages = await this.fetchMessage(10);
if (messages.length) {
messages.forEach(message => {
return this.emitEventListener(message);
});
// } else if (messages.code) {
// console.error(`Error @ fetchMessage:\n\tCode: ${messages.code}\n\tMessage: ${messages.message || messages.msg || messages}`);
}
}, this.interval);
}
emitEventListener (messageResp) {
// No `code` or `code = 0` presents a success response
if (messageResp.code) {
console.error(`Error: bad response with code ${messageResp.code}: ${messageResp.msg}`);
return;
}
// get response.data for 2.x or message for 1.x
const message = messageResp.data || messageResp;
if (this.types.includes(message.type)) {
message.reply = msg => this.reply(msg, message);
message.quoteReply = msg => this.quoteReply(msg, message);
message.recall = () => this.recall(message);
for (let listener of this.eventListeners.message) {
listener(message, this);
}
}
else if (message.type in events) {
if (['NewFriendRequestEvent', 'BotInvitedJoinGroupRequestEvent', 'MemberJoinRequestEvent'].includes(message.type)) {
const self = this;
const args = [message.eventId, message.fromId, message.groupId];
const eventHandler = message.type === 'NewFriendRequestEvent'
? (...opt) => self.handleNewFriendRequest(...args, ...opt)
: message.type === 'BotInvitedJoinGroupRequestEvent'
? (...opt) => self.handleBotInvitedJoinGroupRequest(...args, ...opt)
: (...opt) => self.handleMemberJoinRequest(...args, ...opt)
const methods = {
accept (msg) {
return eventHandler(0, msg);
},
reject (msg) {
return eventHandler(1, msg);
},
rejectAndBlock: null,
ignore: null,
ignoreAndBlock: null,
};
if (message.type === 'NewFriendRequestEvent') {
methods.rejectAndBlock = (msg) => {
return self.handleNewFriendRequest(...args, 2, msg);
};
}
if (message.type === 'MemberJoinRequestEvent') {
methods.ignore = msg => self.handleMemberJoinRequest(...args, 2, msg);
methods.rejectAndBlock = msg => self.handleMemberJoinRequest(...args, 3, msg);
methods.ignoreAndBlock = msg => self.handleMemberJoinRequest(...args, 4, msg);
}
Object.assign(message, methods);
}
for (let listener of this.eventListeners[events[message.type]]) {
listener(message, this);
}
}
}
// plugins
// 这个插件系统需要大量改进
getPlugins () {
return this.plugins.map(i => i.name);
}
/**
* @method NodeMirai#use
* @description install plugin
* @param { object } plugin plugin config
* @param { string } plugin.name unique plugin name
* @param { string } [plugin.subscribe] subscribe event name
* @param { function } plugin.callback callback function
*/
use (plugin) {
if (!plugin.name || typeof plugin.name !== 'string' || plugin.name.length === 0) throw new Error(`[NodeMirai] Invalid plugin name ${plugin.name}. Plugin name must be a string.`);
if (!plugin.callback || typeof plugin.callback !== 'function') throw new Error('[NodeMirai] Invalid plugin callback. Plugin callback must be a function.');
if (this.getPlugins().includes(plugin.name)) throw new Error(`[NodeMirai] Duplicate plugin name ${plugin.name}`);
this.plugins.push(plugin);
// TODO: support string[]
const event = typeof plugin.subscribe === 'string' ? plugin.subscribe : 'message';
this.on(event, plugin.callback);
console.log(`[NodeMirai] Installed plugin [ ${plugin.name} ]`);
}
remove (pluginName) {
const pluginNames = this.getPlugins();
if (pluginNames.includes(pluginName)) {
const plugin = this.plugins[pluginNames.indexOf(pluginName)];
for (let event in this.eventListeners) {
for (let i in this.eventListeners[event]) {
if (this.eventListeners.message[i] === plugin.callback) {
this.eventListeners.message.splice(i, 1);
console.log(`[NodeMirai] Uninstalled plugin [ ${plugin.name} ]`);
}
}
}
}
}
}
module.exports = NodeMirai;