├── type.ts ├── .npmignore ├── .gitignore ├── src ├── release.js ├── ws │ ├── syncId.js │ ├── send.js │ └── index.js ├── init.js ├── utils │ ├── waitFor.js │ ├── checkMAHVersion.js │ └── Signal.js ├── fetchMessage.js ├── recall.js ├── verify.js ├── target.js ├── events.json ├── group.js ├── manage.js ├── sendMessage.js ├── fileUtility.js ├── events.ts ├── typedef.ts └── MessageComponent.js ├── package.json ├── README.md ├── event.md └── index.js /type.ts: -------------------------------------------------------------------------------- 1 | export * from './types/src/typedef' 2 | export { events } from './types/src/events' -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gradle 3 | .gitignore 4 | .git/ 5 | test/ 6 | 7 | jsdoc* 8 | docs/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | test/ 4 | 5 | jsdoc* 6 | docs/ 7 | 8 | .idea 9 | 10 | # typedefs are auto-generated so don't need to track 11 | types/ 12 | /type.js -------------------------------------------------------------------------------- /src/release.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const release = async (host, sessionKey, qq) => { //释放会话 4 | const { data } = await axios.post(`${host}/release`, { 5 | sessionKey, qq, 6 | }); 7 | return data; 8 | }; 9 | 10 | module.exports = release; -------------------------------------------------------------------------------- /src/ws/syncId.js: -------------------------------------------------------------------------------- 1 | /** 2 | * syncId 3 | * 与 ws 服务端通信使用, 和 ws 推送的 syncId 对应 4 | */ 5 | let _id = 0; 6 | let _skipId = -1; 7 | 8 | const syncId = () => { 9 | return _id++ === _skipId ? _id++ : _id; 10 | }; 11 | syncId.skip = n => _skipId = n; 12 | 13 | module.exports = syncId; 14 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | //原auth接口 3 | const init = async (host, verifyKey, isV1) => { //开始会话-认证 4 | const keyName = isV1 ? 'auth' : 'verify' 5 | const { data } = await axios.post(`${host}/${keyName}`, { 6 | [keyName + 'Key']: verifyKey, 7 | }); 8 | return data; 9 | }; 10 | 11 | module.exports = init; -------------------------------------------------------------------------------- /src/utils/waitFor.js: -------------------------------------------------------------------------------- 1 | const polling = async (fn = async () => {}, cb) => { 2 | if (await fn()) return cb(); 3 | else return setTimeout(() => { 4 | return polling(fn, cb); 5 | }, 100); 6 | }; 7 | const waitFor = async (fn = () => {}, cb = () => {}) => { 8 | return setTimeout(() => { 9 | return polling(fn, cb); 10 | }, 100); 11 | }; 12 | 13 | module.exports = waitFor; -------------------------------------------------------------------------------- /src/fetchMessage.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const fetchMessage = async (host, sessionKey, count = 10) => { 4 | //使用此方法获取 bot 接收到的最老消息和最老各类事件 (会从MiraiApiHttp消息记录中删除) 5 | const { data } = await axios.get(`${host}/fetchMessage?sessionKey=${sessionKey}&count=${count}`, ); 6 | if ('code' in data && data.code === 0) return data.data; 7 | return data; 8 | }; 9 | 10 | module.exports = fetchMessage; -------------------------------------------------------------------------------- /src/recall.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const ws = require('./ws/send'); 3 | 4 | const recall = ({ //撤回消息,管理BOT不能撤回群主消息,会报错 5 | target, 6 | messageId, 7 | sessionKey, 8 | host, 9 | wsOnly, 10 | }) => { 11 | if (wsOnly) return ws.recall({ target, messageId }); 12 | return axios.post(`${host}/recall`, { 13 | sessionKey, 14 | target, 15 | messageId, 16 | }).then(({ data }) => { 17 | // 和 ws 保持一致返回 data 18 | return data 19 | }); 20 | }; 21 | 22 | module.exports = recall; -------------------------------------------------------------------------------- /src/verify.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | /** 4 | * 校验会话 5 | * @param { string } host mirai-api-http 服务器地址 6 | * @param { string } sessionKey 会话密钥 7 | * @param { string } qq session 对应的 QQ 号 8 | * @returns {Promise<*>} 9 | */ 10 | const verify = async (host, sessionKey, qq, isV1) => { 11 | // console.log(host,sessionKey,qq); 12 | const { data } = await axios.post(`${host}/${isV1 ? 'verify' : 'bind'}`, { 13 | sessionKey, qq, 14 | }); 15 | return data; 16 | }; 17 | 18 | module.exports = verify; -------------------------------------------------------------------------------- /src/utils/checkMAHVersion.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const semver = require('semver') 3 | 4 | const checkMAHVersion = (self) => { 5 | // ws-only mode is supported in v2 6 | // if (this.wsOnly) return false; 7 | return axios.get(`${self.host}/about`).then(({ data }) => { 8 | if (data && data.data && data.data.version) { 9 | self.mahVersion = data.data.version 10 | if (semver.major(data.data.version) === 1) { 11 | console.log(`You are using mah-${data.data.version}, recommended to update to 2.x`); 12 | return true; 13 | } 14 | } 15 | return false; 16 | }); 17 | }; 18 | 19 | module.exports = checkMAHVersion; 20 | -------------------------------------------------------------------------------- /src/ws/send.js: -------------------------------------------------------------------------------- 1 | const ws = require('.'); 2 | 3 | // const sendFriendMessage = (content) => { 4 | // return ws.send({ 5 | // command: 'sendFriendMessage', 6 | // content, 7 | // }); 8 | // }; 9 | 10 | // module.exports = { 11 | // sendFriendMessage, 12 | // }; 13 | 14 | // Just use a proxy to send all types of messages 15 | const sendMessage = new Proxy({}, { 16 | /** 17 | * sendMessage 18 | * @param { any } _ any 19 | * @param { string } method method 20 | * @returns { function } 21 | */ 22 | get(_, method) { 23 | const command = method.replace('Quoted', ''); 24 | return (content, subCommand) => ws.send({ command, content, subCommand }); 25 | } 26 | }); 27 | 28 | module.exports = sendMessage; 29 | -------------------------------------------------------------------------------- /src/utils/Signal.js: -------------------------------------------------------------------------------- 1 | const waitFor = require('./waitFor'); 2 | 3 | class Signal { 4 | constructor () { 5 | // this.signalList = { 6 | // 'authed': [], 7 | // 'verified': [], 8 | // }; 9 | this.signalList = ['authed', 'verified']; 10 | this.signals = []; 11 | } 12 | on (signalName, callback) { 13 | // if (!this.signalList[signalName]) this.signalList[signalName] = []; 14 | // this.signalList[signalName].push(callback); 15 | waitFor(() => { 16 | if (this.signals.includes(signalName)) return true; 17 | return false; 18 | }, callback); 19 | } 20 | trigger (signalName) { 21 | this.signals.push(signalName); 22 | // if (!this.signalList[signalName]) return; 23 | // for (let callback of this.signalList[signalName]) callback(); 24 | } 25 | } 26 | 27 | module.exports = Signal; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-mirai-sdk", 3 | "version": "0.3.6", 4 | "description": "node api for mirai qq bot", 5 | "main": "index.js", 6 | "types": "./types/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dts": "tsc ./index.js --declaration --allowJs --emitDeclarationOnly --outDir types --skipLibCheck", 10 | "postdts": "tsc ./type.ts --skipLibCheck && tsc ./src/*.ts --outDir ./types/src/" 11 | }, 12 | "keywords": [ 13 | "mirai", 14 | "bot" 15 | ], 16 | "author": "RedBeanN", 17 | "license": "ISC", 18 | "dependencies": { 19 | "axios": "^0.21.1", 20 | "form-data": "^3.0.0", 21 | "semver": "^7.3.7", 22 | "ws": "^7.5.9" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^16.11.59", 26 | "@types/ws": "^7.4.7", 27 | "docdash": "^1.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ws/index.js: -------------------------------------------------------------------------------- 1 | const id = require('./syncId'); 2 | /** 3 | * @type { WebSocket } 4 | */ 5 | let ws = null; 6 | const messageMap = new Map(); 7 | 8 | const init = (wsHost, syncId = -1, bot) => { 9 | ws = wsHost; 10 | id.skip(syncId); 11 | ws.on('message', msg => { 12 | const data = JSON.parse(msg); 13 | const _id = parseInt(data.syncId) 14 | if (isNaN(_id)) return; 15 | // server push messages 16 | if (_id === syncId) return bot.emitEventListener(data); 17 | // responses 18 | if (messageMap.has(_id)) { 19 | const [r, j] = messageMap.get(_id); 20 | if (data.data && data.data.code === 0) { 21 | // unwrap deep data... 22 | r(data.data.data || data.data); 23 | } else { 24 | // some api does not response a wrapped { code, data } object 25 | if (!('code' in data.data)) r(data.data); 26 | // don't reject a success response with non-zero code 27 | // else j(data.data); 28 | else r(data.data || data); 29 | } 30 | } 31 | }); 32 | }; 33 | const send = (data) => { 34 | if (!ws) return; 35 | const syncId = id(); 36 | ws.send(JSON.stringify({ syncId, ...data })); 37 | return new Promise((r, j) => { 38 | messageMap.set(syncId, [r, j]); 39 | }); 40 | }; 41 | 42 | module.exports = { 43 | init, 44 | send, 45 | }; 46 | -------------------------------------------------------------------------------- /src/target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { Object } Sender 3 | * @property { number } [id] 4 | * @property { Object } [group] 5 | * @property { number } [group.id] 6 | * 7 | * @typedef { Object } FriendTarget 8 | * @property { 'FriendMessage' } type 9 | * @property { Object } sender 10 | * @property { number } sender.id 11 | * 12 | * @typedef { Object } GroupTarget 13 | * @property { 'GroupMessage' } type 14 | * @property { Object } sender 15 | * @property { Object } sender.group 16 | * @property { number } sender.group.id 17 | * 18 | * @typedef { Object } TempTarget 19 | * @property { 'TempMessage' } type 20 | * @property { number } id 21 | * @property { Object } sender 22 | * @property { Object } sender.group 23 | * @property { number } sender.group.id 24 | * 25 | * @typedef { FriendTarget | GroupTarget | TempTarget } MessageTarget 26 | */ 27 | 28 | /** 29 | * @function Friend 30 | * @param { number } qq 31 | * @returns { FriendTarget } 32 | */ 33 | const Friend = (qq) => ({ 34 | type: 'FriendMessage', 35 | sender: { 36 | id: qq 37 | } 38 | }); 39 | 40 | /** 41 | * @function Group 42 | * @param { number } groupId 43 | * @returns { GroupTarget } 44 | */ 45 | const Group = (groupId) => ({ 46 | type: 'GroupMessage', 47 | sender: { 48 | group: { 49 | id: groupId 50 | } 51 | } 52 | }); 53 | 54 | /** 55 | * @function Temp 56 | * @param { number } groupId 57 | * @param { number } qq 58 | * @returns { TempTarget } 59 | */ 60 | const Temp = (groupId, qq) => ({ 61 | type: 'TempMessage', 62 | sender: { 63 | id: qq, 64 | group: { 65 | id: groupId 66 | } 67 | } 68 | }); 69 | 70 | module.exports = { 71 | Friend, 72 | Group, 73 | Temp 74 | }; 75 | -------------------------------------------------------------------------------- /src/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "BotOnlineEvent": "online", 3 | "BotOfflineEventActive": "offlineActive", 4 | "BotOfflineEventForce": "offlineForce", 5 | "BotOfflineEventDropped": "offlineDropped", 6 | "BotReloginEvent": "relogin", 7 | "BotGroupPermissionChangeEvent": "groupPermissionChange", 8 | "BotMuteEvent": "mute", 9 | "BotUnmuteEvent": "unmute", 10 | "BotLeaveEventActive": "leaveActive", 11 | "BotLeaveEventKick": "leaveKick", 12 | "BotLeaveEventDisband": "leaveDisband", 13 | "BotJoinGroupEvent": "joinGroup", 14 | "BotInvitedJoinGroupRequestEvent": "invitedJoinGroupRequest", 15 | "GroupNameChangeEvent": "groupNameChange", 16 | "GroupEntranceAnnouncementChangeEvent": "groupEntranceAnnouncementChange", 17 | "GroupMuteAllEvent": "groupMuteAll", 18 | "GroupAllowAnonymousChatEvent": "groupAllowAnonymousChat", 19 | "GroupAllowConfessTalkEvent": "groupAllowConfessTalk", 20 | "GroupAllowMemberInviteEvent": "groupAllowMemberInvite", 21 | "GroupRecallEvent": "groupRecall", 22 | "FriendRecallEvent": "friendRecall", 23 | "FriendNickChangedEvent": "friendNickChanged", 24 | "FriendInputStatusChangedEvent": "friendInputStatusChanged", 25 | "MemberJoinEvent": "memberJoin", 26 | "MemberLeaveEventKick": "memberLeaveKick", 27 | "MemberLeaveEventQuit": "memberLeaveQuit", 28 | "MemberCardChangeEvent": "memberCardChange", 29 | "MemberSpecialTitleChangeEvent": "memberSpecialTitleChange", 30 | "MemberPermissionChangeEvent": "memberPermissionChange", 31 | "MemberMuteEvent": "memberMute", 32 | "MemberUnmuteEvent": "memberUnmute", 33 | "MemberHonorChangeEvent": "memberHonorChange", 34 | "MemberJoinRequestEvent": "memberJoinRequest", 35 | "NewFriendRequestEvent": "newFriendRequest", 36 | "NudgeEvent": "nudge", 37 | "OtherClientOnlineEvent": "otherClientOnline", 38 | "OtherClientOfflineEvent": "otherClientOffline", 39 | "CommandExecutedEvent": "commandExecuted" 40 | } -------------------------------------------------------------------------------- /src/group.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const ws = require('./ws/send'); 3 | 4 | const getMemberList = async ({ //获取群成员列表 5 | target, 6 | host, 7 | sessionKey, 8 | wsOnly, 9 | }) => { 10 | if (wsOnly) return ws.memberList({ target }); 11 | const { data } = await axios.get(`${host}/memberList`, { 12 | params: { 13 | sessionKey, 14 | target, 15 | }, 16 | }); 17 | return data.data || data; 18 | }; 19 | 20 | const setMute = async ({ //群禁言群成员 21 | target, 22 | memberId, 23 | time, 24 | host, 25 | sessionKey, 26 | wsOnly, 27 | }) => { 28 | if (wsOnly) return ws.mute({ target, memberId, time }); 29 | const { data } = await axios.post(`${host}/mute`, { 30 | sessionKey, 31 | target, 32 | memberId, 33 | time, 34 | }); 35 | return data; 36 | }; 37 | const setUnmute = async ({ //群解除群成员禁言 38 | target, 39 | memberId, 40 | host, 41 | sessionKey, 42 | wsOnly, 43 | }) => { 44 | if (wsOnly) return ws.unmute({ target, memberId }); 45 | const { data } = await axios.post(`${host}/unmute`, { 46 | sessionKey, 47 | target, 48 | memberId, 49 | }); 50 | return data; 51 | }; 52 | 53 | const setMuteAll = async ({ //群全体禁言 54 | target, 55 | host, 56 | sessionKey, 57 | wsOnly, 58 | }) => { 59 | if (wsOnly) return ws.muteAll({ target }); 60 | const { data } = await axios.post(`${host}/muteAll`, { 61 | sessionKey, 62 | target, 63 | }); 64 | return data; 65 | }; 66 | 67 | const setUnmuteAll = async ({ //群解除全体禁言 68 | target, 69 | host, 70 | sessionKey, 71 | wsOnly, 72 | }) => { 73 | if (wsOnly) return ws.unmuteAll({ target }); 74 | const { data } = await axios.post(`${host}/unmuteAll`, { 75 | sessionKey, 76 | target, 77 | }); 78 | return { data }; 79 | }; 80 | 81 | const setKick = async ({ // 移除群成员 82 | target, 83 | memberId, 84 | msg, 85 | host, 86 | sessionKey, 87 | wsOnly, 88 | }) => { 89 | if (wsOnly) return ws.kick({ target, memberId, msg }); 90 | const { data } = await axios.post(`${host}/kick`, { 91 | sessionKey, 92 | target, 93 | memberId, 94 | msg, 95 | }); 96 | return data; 97 | } 98 | 99 | const setEssence = async ({ // 设置群精华 100 | target, 101 | id, 102 | sessionKey, 103 | wsOnly, 104 | }) => { 105 | if (wsOnly) return ws.setEssence({ target }); 106 | const { data } = await axios.post(`${host}/setEssence`, { 107 | sessionKey, 108 | target, 109 | messageId: id 110 | }); 111 | return data; 112 | } 113 | 114 | const getConfig = async ({ // 获取群设置 115 | target, 116 | host, 117 | sessionKey, 118 | wsOnly, 119 | }) => { 120 | if (wsOnly) return ws.groupConfig({ target }, 'get'); 121 | const { data } = await axios.get(`${host}/groupConfig`, { 122 | params: { 123 | sessionKey, 124 | target, 125 | }, 126 | }); 127 | return data; 128 | }; 129 | const setConfig = async ({ //修改群设置 130 | target, 131 | config, 132 | host, 133 | sessionKey, 134 | wsOnly, 135 | }) => { 136 | if (wsOnly) return ws.groupConfig({ target, config }, 'update'); 137 | const { data } = await axios.post(`${host}/groupConfig`, { 138 | target, 139 | sessionKey, 140 | config, 141 | }); 142 | return data; 143 | }; 144 | 145 | const getMemberInfo = async ({ //获取群员资料 146 | target, 147 | memberId, 148 | host, 149 | sessionKey, 150 | wsOnly, 151 | }) => { 152 | if (wsOnly) return ws.memberInfo({ target, memberId }, 'get'); 153 | const { data } = await axios.get(`${host}/memberInfo`, { 154 | params: { 155 | sessionKey, 156 | target, 157 | memberId, 158 | }, 159 | }); 160 | return data; 161 | }; 162 | 163 | const setMemberInfo = async ({ //修改群员资料 164 | target, 165 | memberId, 166 | info, 167 | host, 168 | sessionKey, 169 | wsOnly, 170 | }) => { 171 | if (wsOnly) return ws.memberInfo({ target, memberId, info }, 'update'); 172 | const { data } = await axios.post(`${host}/memberInfo`, { 173 | target, 174 | memberId, 175 | info, 176 | sessionKey, 177 | }); 178 | return data; 179 | } 180 | 181 | const handleMemberJoinRequest = async({ //获取入群申请 182 | sessionKey, 183 | host, 184 | eventId, 185 | fromId, 186 | groupId, 187 | operate, 188 | message, 189 | wsOnly, 190 | }) => { 191 | if (wsOnly) return ws.resp_memberJoinRequestEvent({ 192 | eventId, 193 | fromId, 194 | groupId, 195 | operate, 196 | message, 197 | }); 198 | const { data } = await axios.post(`${host}/resp/memberJoinRequestEvent`, { 199 | sessionKey, 200 | eventId, 201 | fromId, 202 | groupId, 203 | operate, 204 | message 205 | }); 206 | return data; 207 | } 208 | 209 | module.exports = { 210 | getMemberList, 211 | setMute, 212 | setUnmute, 213 | setMuteAll, 214 | setUnmuteAll, 215 | setKick, 216 | setEssence, 217 | getConfig, 218 | setConfig, 219 | getMemberInfo, 220 | setMemberInfo, 221 | handleMemberJoinRequest 222 | }; -------------------------------------------------------------------------------- /src/manage.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const ws = require('./ws/send'); 3 | 4 | const getFriendList = async ({ //获取好友列表 5 | host, 6 | sessionKey, 7 | wsOnly, 8 | }) => { 9 | if (wsOnly) return ws.friendList(); 10 | const { data } = await axios.get(`${host}/friendList?sessionKey=${sessionKey}`); 11 | return data.data || data; 12 | }; 13 | 14 | const getGroupList = async ({ //获取群列表 15 | host, 16 | sessionKey, 17 | wsOnly, 18 | }) => { 19 | if (wsOnly) return ws.groupList(); 20 | const { data } = await axios.get(`${host}/groupList?sessionKey=${sessionKey}`); 21 | return data.data || data; 22 | }; 23 | 24 | const getBotProfile = async ({ 25 | host, 26 | sessionKey, 27 | wsOnly, 28 | }) => { 29 | if (wsOnly) return ws.botProfile(); 30 | const { data } = await axios.get(`${host}/botProfile?sessionKey=${sessionKey}`); 31 | return data.data || data; 32 | }; 33 | const getFriendProfile = async ({ 34 | host, 35 | sessionKey, 36 | qq, 37 | wsOnly, 38 | }) => { 39 | if (wsOnly) return ws.friendProfile({ target: qq }); 40 | const { data } = await axios.get(`${host}/friendProfile?sessionKey=${sessionKey}&target=${qq}`); 41 | return data.data || data; 42 | }; 43 | const getMemberProfile = async ({ 44 | host, 45 | sessionKey, 46 | group, 47 | qq, 48 | wsOnly, 49 | }) => { 50 | if (wsOnly) return ws.memberProfile({ target: group, memberId: qq }); 51 | const { data } = await axios.get(`${host}/memberProfile?sessionKey=${sessionKey}&target=${group}&memberId=${qq}`); 52 | return data.data || data; 53 | }; 54 | 55 | const getMessageById = async ({ //通过messageId获取一条被缓存的消息 56 | messageId, 57 | target, 58 | host, 59 | sessionKey, 60 | wsOnly, 61 | }) => { 62 | if (wsOnly) return ws.messageFromId({ messageId, target }); 63 | const { data } = await axios.get(`${host}/messageFromId?sessionKey=${sessionKey}&messageId=${messageId}&target=${target}`); 64 | return data; 65 | }; 66 | 67 | const registerCommand = async ({ //注册指令 68 | host, 69 | verifyKey, 70 | name, 71 | alias, 72 | description, 73 | usage 74 | }) => { 75 | const { data } = await axios.post(`${host}/command/register`, { 76 | verifyKey, name, alias, description, usage, 77 | }); 78 | return { data }; 79 | }; 80 | 81 | const sendCommand = async ({ //发送指令 82 | host, 83 | verifyKey, 84 | name, 85 | args, 86 | }) => { 87 | const { data } = await axios.post(`${host}/command/send`, { 88 | verifyKey, name, args, 89 | }); 90 | return data; 91 | }; 92 | 93 | const getManagers = async ({ //获取Mangers 94 | host, 95 | verifyKey, 96 | qq, 97 | }) => { 98 | const { data } = await axios.get(`${host}/managers?verifyKey=${verifyKey}&qq=${qq}`); 99 | return data; 100 | }; 101 | 102 | const botInvitedJoinGroupRequestHandler = async({ 103 | sessionKey, 104 | host, 105 | eventId, 106 | fromId, 107 | groupId, 108 | operate, 109 | message, 110 | wsOnly, 111 | }) => { 112 | if (wsOnly) return ws.resp_botInvitedJoinGroupRequestEvent({ 113 | eventId, 114 | fromId, 115 | groupId, 116 | operate, 117 | message, 118 | }); 119 | const { data } = await axios.post(`${host}/resp/botInvitedJoinGroupRequestEvent`, { 120 | sessionKey, 121 | eventId, 122 | fromId, 123 | groupId, 124 | operate, 125 | message 126 | }); 127 | return data; 128 | }; 129 | 130 | const quitGroup = async ({ 131 | sessionKey, 132 | host, 133 | target, 134 | wsOnly, 135 | }) => { 136 | if (wsOnly) return ws.quit({ target }); 137 | const { data } = await axios.post(`${host}/quit`, { 138 | sessionKey, 139 | target 140 | }); 141 | return data; 142 | } 143 | 144 | const handleNewFriendRequest = async({ 145 | sessionKey, 146 | host, 147 | eventId, 148 | fromId, 149 | groupId, 150 | operate, 151 | message, 152 | wsOnly, 153 | }) => { 154 | if (wsOnly) return ws.resp_newFriendRequestEvent({ 155 | eventId, 156 | fromId, 157 | groupId, 158 | operate, 159 | message, 160 | }); 161 | const { data } = await axios.post(`${host}/resp/newFriendRequestEvent`, { 162 | sessionKey, 163 | eventId, 164 | fromId, 165 | groupId, 166 | operate, 167 | message 168 | }); 169 | return data; 170 | }; 171 | 172 | const deleteFriend = async ({ 173 | sessionKey, 174 | host, 175 | target, 176 | wsOnly, 177 | }) => { 178 | if (wsOnly) return ws.deleteFriend({ target }); 179 | const { data } = await axios.post(`${host}/deleteFriend`, { 180 | sessionKey, 181 | target, 182 | }); 183 | return data; 184 | }; 185 | 186 | const getRoamingMessages = async ({ 187 | sessionKey, 188 | host, 189 | target, 190 | timeStart, 191 | timeEnd, 192 | wsOnly, 193 | }) => { 194 | if (wsOnly) return ws.roamingMessages({ timeStart, timeEnd, target }); 195 | const { data } = await axios.post(`${host}/roamingMessages`, { 196 | sessionKey, 197 | timeStart, 198 | timeEnd, 199 | target, 200 | }); 201 | return data 202 | }; 203 | 204 | module.exports = { 205 | getFriendList, 206 | getGroupList, 207 | getBotProfile, 208 | getFriendProfile, 209 | getMemberProfile, 210 | getMessageById, 211 | registerCommand, 212 | sendCommand, 213 | getManagers, 214 | botInvitedJoinGroupRequestHandler, 215 | quitGroup, 216 | handleNewFriendRequest, 217 | deleteFriend, 218 | getRoamingMessages, 219 | }; -------------------------------------------------------------------------------- /src/sendMessage.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const fs = require('fs'); 3 | const FormData = require('form-data'); 4 | const wsMessage = require('./ws/send'); 5 | 6 | const Mirai = require('../') 7 | const { Plain, Image, FlashImage, Voice } = require('./MessageComponent'); 8 | 9 | const withRecall = async (message, target, bot) => { 10 | message.recall = () => { 11 | bot.recall(message, target); 12 | }; 13 | return message; 14 | }; 15 | /** 16 | * @function callApi 17 | * @param { string } api 18 | * @param { object } [payload] 19 | * @param { Mirai } bot 20 | */ 21 | const callApi = async (api, payload, bot) => { 22 | // callApi(api, bot) without payload 23 | if (!bot && payload instanceof Mirai) { 24 | bot = payload; 25 | payload = {}; 26 | } 27 | const data = bot.wsOnly 28 | ? await wsMessage[api](payload) 29 | : await axios.post(`${bot.host}/${api}`, Object.assign(payload, { 30 | sessionKey: bot.sessionKey 31 | })).then(({ data }) => data); 32 | // console.log(res); 33 | // Normal messages with `target`, tempMessage with `qq` 34 | const target = payload.target || payload.qq || payload.group; 35 | if (data && target) return withRecall(data, target, bot); 36 | return data; 37 | }; 38 | 39 | const sendFriendMessage = async ({ //发送好友消息 40 | messageChain, 41 | target, 42 | }, bot) => { 43 | if (typeof messageChain === 'string') messageChain = [Plain(messageChain)]; 44 | return callApi('sendFriendMessage', { messageChain, target }, bot); 45 | }; 46 | 47 | const sendQuotedFriendMessage = async ({ // 好友中引用一条消息的messageId进行回复 48 | messageChain, 49 | target, 50 | quote, 51 | }, bot) => { 52 | if (typeof messageChain === 'string') messageChain = [Plain(messageChain)]; 53 | return callApi('sendFriendMessage', { messageChain, target, quote }, bot); 54 | }; 55 | 56 | const sendGroupMessage = async ({ // 发送群消息 57 | messageChain, 58 | target, 59 | }, bot) => { 60 | if (typeof messageChain === 'string') messageChain = [Plain(messageChain)]; 61 | return callApi('sendGroupMessage', { messageChain, target }, bot); 62 | }; 63 | const sendQuotedGroupMessage = async ({ // 群消息中引用一条消息的 messageId 进行回复 64 | messageChain, 65 | target, 66 | quote, 67 | }, bot) => { 68 | if (typeof messageChain === 'string') messageChain = [Plain(messageChain)]; 69 | return callApi('sendGroupMessage', { messageChain, target, quote }, bot); 70 | }; 71 | 72 | const sendTempMessage = async ({ // 发送临时会话消息 73 | messageChain, 74 | qq, 75 | group, 76 | }, bot) => { 77 | if (typeof messageChain === 'string') messageChain = [Plain(messageChain)]; 78 | return callApi('sendTempMessage', { messageChain, qq, group }, bot); 79 | }; 80 | 81 | const sendQuotedTempMessage = async ({ // 发送临时会话引用一条消息的messageId进行回复 82 | messageChain, 83 | qq, 84 | group, 85 | quote, 86 | }, bot) => { 87 | if (typeof messageChain === 'string') messageChain = [Plain(messageChain)]; 88 | return callApi('sendTempMessage', { messageChain, qq, group, quote }, bot); 89 | }; 90 | 91 | const uploadImage = async ({ 92 | url, 93 | type, 94 | }, bot) => { 95 | if (bot.wsOnly) { 96 | console.warn(`[Warn] uploadImage is not supported in wsOnly mode. Use http instead`); 97 | } 98 | let img = url; 99 | if (typeof url === 'string') img = fs.createReadStream(url); 100 | const form = new FormData(); 101 | form.append('sessionKey', bot.sessionKey); 102 | form.append('type', type); 103 | // #19: 当传入的 url 为 Buffer 类型时,只需指定文件名即可,此写法兼容 ReadStream;另外图片文件名的后缀类型并不会影响上传结果 104 | form.append('img', img, { filename: "payload.jpg" }); 105 | 106 | const { data } = await axios.post(`${bot.host}/uploadImage`, form, { 107 | headers: form.getHeaders(), 108 | }); 109 | return data; 110 | }; 111 | 112 | const uploadVoice = async({ 113 | url, 114 | type, 115 | }, bot) => { 116 | if (bot.wsOnly) { 117 | console.warn(`[Warn] uploadVoice is not supported in wsOnly mode. Use http instead`); 118 | } 119 | let voice = (typeof url === 'string') ? fs.createReadStream(url) : url; 120 | const form = new FormData(); 121 | form.append('sessionKey', bot.sessionKey); 122 | form.append('type', type); 123 | form.append('voice', voice); 124 | 125 | const { data } = await axios.post(`${bot.host}/uploadVoice`, form, { 126 | headers: form.getHeaders() 127 | }); 128 | return data; 129 | }; 130 | 131 | const sendImageMessage = async ({ 132 | url, 133 | qq, 134 | group, 135 | }, bot) => { 136 | let type, send, target; 137 | if (qq) { 138 | type = 'friend'; 139 | send = sendFriendMessage; 140 | target = qq; 141 | } else if (group) { 142 | type = 'group'; 143 | send = sendGroupMessage; 144 | target = group; 145 | } else return console.error('Error @ sendImageMessage: you should provide qq or group'); 146 | const image = await uploadImage({ 147 | url, 148 | type, 149 | }, bot); 150 | const messageChain = [Image(image)]; 151 | return send({ 152 | messageChain, 153 | target, 154 | }, bot); 155 | }; 156 | 157 | const sendVoiceMessage = async({ 158 | url, 159 | group, 160 | }, bot) => { 161 | const target = group, 162 | type = 'group'; 163 | const voice = await uploadVoice({ 164 | url, 165 | type, 166 | }, bot); 167 | const messageChain = [Voice(voice)]; 168 | return sendGroupMessage({ 169 | messageChain, 170 | target, 171 | }, bot); 172 | }; 173 | 174 | const sendFlashImageMessage = async ({ 175 | url, 176 | qq, 177 | group, 178 | }, bot) => { 179 | let type, send, target; 180 | if (qq) { 181 | type = 'friend'; 182 | send = sendFriendMessage; 183 | target = qq; 184 | } else if (group) { 185 | type = 'group'; 186 | send = sendGroupMessage; 187 | target = group; 188 | } else return console.error('Error @ sendImageMessage: you should provide qq or group'); 189 | const image = await uploadImage({ 190 | url, 191 | type, 192 | }, bot); 193 | const messageChain = [FlashImage(image)]; 194 | return send({ 195 | messageChain, 196 | target, 197 | }, bot); 198 | }; 199 | 200 | /** 201 | * @function sendNudge 发送戳一戳消息 202 | * @param { Object } option 203 | * @param { string } option.sessionKey 204 | * @param { number } option.target QQ号, 可以是Bot的QQ 205 | * @param { number } option.subject 接收主体(QQ号或群号) 206 | * @param { 'Group' | 'Friend' | 'Stranger' } option.type 207 | */ 208 | const sendNudge = async ({ 209 | target, 210 | subject, 211 | kind, 212 | host, 213 | sessionKey, 214 | wsOnly, 215 | }) => { 216 | if (wsOnly) return wsMessage.sendNudge({ 217 | target, subject, kind, 218 | }); 219 | const { data } = await axios.post(`${host}/sendNudge`, { 220 | sessionKey, 221 | target, 222 | subject, 223 | kind, 224 | }); 225 | return data; 226 | }; 227 | 228 | module.exports = { 229 | sendFriendMessage, 230 | sendQuotedFriendMessage, 231 | sendGroupMessage, 232 | sendQuotedGroupMessage, 233 | sendTempMessage, 234 | sendQuotedTempMessage, 235 | uploadImage, 236 | uploadVoice, 237 | sendImageMessage, 238 | sendVoiceMessage, 239 | sendFlashImageMessage, 240 | sendNudge, 241 | }; 242 | -------------------------------------------------------------------------------- /src/fileUtility.js: -------------------------------------------------------------------------------- 1 | // node-mirai 文件发送 & 群文件管理相关实现 2 | 3 | const fs = require("fs"); 4 | const axios = require("axios"); 5 | const FormData = require("form-data"); 6 | const ws = require('./ws/send'); 7 | 8 | /** 9 | * @typedef { import('./typedef').GroupFile } FileOrDir 10 | */ 11 | 12 | /** 13 | * 上传群文件并发送 14 | * @param { object } config 15 | * @param { string | Buffer | ReadStream } config.url 文件所在路径或 URL 16 | * @param { string | FileOrDir } config.path 文件要上传到群文件中的位置(路径) 17 | * @param { string } config.target 要发送文件的目标 18 | * @param { string } config.sessionKey 19 | * @param { string } config.host 20 | * @param { boolean } config.isV1 21 | * @param { boolean } config.wsOnly 22 | * @return { object } 23 | */ 24 | const uploadFileAndSend = async({ 25 | url, 26 | path, 27 | target, 28 | sessionKey, 29 | host, 30 | isV1, 31 | wsOnly, 32 | }) => { 33 | if (wsOnly) console.warn(`Upload file through ws is not supported yet`); 34 | const postUrl = isV1 35 | ? `${host}/uploadFileAndSend` 36 | : `${host}/file/upload`; 37 | const file = (typeof url === "string") ? fs.createReadStream(url) : url, 38 | form = new FormData(); 39 | form.append('sessionKey', sessionKey); 40 | form.append('type', isV1 ? "Group" : 'group'); // 当前只支持上传群文件 41 | form.append('path', typeof path === 'string' ? path : path.path || ''); 42 | form.append('target', target); 43 | form.append('file', file); 44 | 45 | const { data } = await axios.post(postUrl, form, { 46 | headers: form.getHeaders() 47 | }); 48 | 49 | return data; 50 | }; 51 | 52 | /** 53 | * 获取群文件指定路径下的文件列表 54 | * @param { Object } config 55 | * @param { number | string } config.target 要获取的群号 56 | * @param { FileOrDir | string } config.dir 要获取的文件路径 57 | * @param { string } config.sessionKey 58 | * @param { string } config.host 59 | * @param { boolean } config.withDownloadInfo 60 | * @param { boolean } config.isV1 61 | * @param { boolean } config.wsOnly 62 | * @returns { Promise } 63 | */ 64 | const getGroupFileList = async({ 65 | target, 66 | dir, 67 | sessionKey, 68 | host, 69 | withDownloadInfo = false, 70 | isV1 = false, 71 | wsOnly = false, 72 | }) => { 73 | if (wsOnly) return ws.file_list({ 74 | id: typeof dir === 'string' ? dir : dir && dir.id, 75 | path: typeof dir === 'string' ? dir : null, 76 | target, withDownloadInfo, 77 | }); 78 | let getUrl = isV1 79 | ? `${host}/groupFileList?sessionKey=${sessionKey}&target=${target}` 80 | : `${host}/file/list?sessionKey=${sessionKey}&target=${target}`; 81 | if (withDownloadInfo) { 82 | getUrl += '&withDownloadInfo=true'; 83 | } 84 | // set dir to null for listing root path 85 | if (!dir) { 86 | getUrl += '&id='; 87 | } else { 88 | if (typeof dir === 'string') { 89 | getUrl += `&${isV1 ? 'dir' : 'path'}=${dir}`; 90 | } else { 91 | getUrl += `&id=${dir.id}`; 92 | } 93 | } 94 | const { data } = await axios.get(getUrl); 95 | return data.data || data; 96 | }; 97 | 98 | /** 99 | * 获取群文件指定详细信息 100 | * @param { Object } config 101 | * @param { number | string } config.target 要获取的群号 102 | * @param { string | FileOrDir } config.id 文件唯一 ID 103 | * @param { string } config.sessionKey 104 | * @param { string } config.host 105 | * @param { boolean } config.withDownloadInfo 106 | * @param { boolean } config.isV1 107 | * @param { boolean } config.wsOnly 108 | * @returns { object } 109 | */ 110 | const getGroupFileInfo = async({ 111 | target, 112 | id, 113 | sessionKey, 114 | host, 115 | withDownloadInfo, 116 | isV1, 117 | wsOnly, 118 | }) => { 119 | const realId = typeof id === 'object' ? id.id : id; 120 | if (wsOnly) return ws.file_info({ 121 | id: realId, 122 | target, withDownloadInfo, 123 | }); 124 | const getUrl = isV1 125 | ? `${host}/groupFileInfo?sessionKey=${sessionKey}&target=${target}&id=${realId}` 126 | : `${host}/file/info?sessionKey=${sessionKey}&target=${target}&id=${realId}`; 127 | if (withDownloadInfo) { 128 | getUrl += '&withDownloadInfo=true'; 129 | } 130 | const { data } = await axios.get(getUrl); 131 | return data; 132 | }; 133 | 134 | 135 | /** 136 | * 重命名指定群文件 137 | * @param { object } config 138 | * @param { number | string } config.target 目标群号 139 | * @param { string | FileOrDir } config.id 要重命名的文件唯一 ID 140 | * @param { string } config.rename 文件的新名称 141 | * @param { string } config.sessionKey 142 | * @param { string } config.host 143 | * @param { boolean } config.isV1 144 | * @param { boolean } config.wsOnly 145 | * @returns { object } 146 | */ 147 | const renameGroupFile = async({ 148 | target, 149 | id, 150 | rename, 151 | sessionKey, 152 | host, 153 | isV1, 154 | wsOnly, 155 | }) => { 156 | const realId = typeof id === 'string' ? id : id.id; 157 | if (wsOnly) return ws.file_rename({ 158 | id: realId, 159 | renameTo: rename, 160 | target, 161 | }); 162 | const postUrl = isV1 163 | ? `${host}/groupFileRename` 164 | : `${host}/file/rename`; 165 | const postData = { 166 | sessionKey, 167 | target, 168 | id: realId, 169 | [isV1 ? 'rename' : 'renameTo']: rename, 170 | }; 171 | console.log(postData); 172 | const { data } = await axios.post(postUrl, postData); 173 | 174 | return data; 175 | }; 176 | 177 | /** 178 | * 移动指定群文件 179 | * @param { object } config 180 | * @param { number | string } config.target 目标群号 181 | * @param { string | FileOrDir } config.id 要移动的文件唯一 ID 182 | * @param { string | FileOrDir } config.moveTo 文件的新路径 183 | * @param { string } config.sessionKey 184 | * @param { string } config.host 185 | * @param { boolean } config.isV1 186 | * @param { boolean } config.wsOnly 187 | * @returns { object } 188 | */ 189 | const moveGroupFile = async({ 190 | target, 191 | id, 192 | moveTo, 193 | sessionKey, 194 | host, 195 | isV1, 196 | wsOnly, 197 | }) => { 198 | const realId = typeof id === 'string' ? id : id.id; 199 | if (wsOnly) return ws.file_move({ 200 | id: realId, 201 | moveTo: moveTo.id || null, 202 | moveToPath: typeof moveTo === 'string' ? moveTo : null, 203 | target, 204 | }); 205 | const postUrl = isV1 206 | ? `${host}/groupFileMove` 207 | : `${host}/file/move`; 208 | const postData = { 209 | sessionKey, 210 | id: realId, 211 | target, 212 | }; 213 | if (typeof moveTo === 'object') { 214 | postData.moveTo = moveTo.id; 215 | } else { 216 | postData[isV1 ? 'movePath' : 'moveToPath'] = moveTo; 217 | } 218 | console.log(postData, postUrl); 219 | const { data } = await axios.post(postUrl, postData); 220 | 221 | return data; 222 | }; 223 | 224 | /** 225 | * 删除指定群文件 226 | * @param { object } config 227 | * @param { number | string } config.target 目标群号 228 | * @param { string | FileOrDir } config.id 要删除的文件唯一 ID 229 | * @param { string } config.sessionKey 230 | * @param { string } config.host 231 | * @param { boolean } config.isV1 232 | * @param { boolean } config.wsOnly 233 | * @returns { object } 234 | */ 235 | const deleteGroupFile = async({ 236 | target, 237 | id, 238 | sessionKey, 239 | host, 240 | isV1, 241 | wsOnly, 242 | }) => { 243 | const realId = typeof id === 'string' ? id : id.id; 244 | if (wsOnly) ws.file_delete({ 245 | id: realId, 246 | target, 247 | }); 248 | const postUrl = isV1 249 | ? `${host}/groupFileDelete` 250 | : `${host}/file/delete`; 251 | const postData = { 252 | sessionKey, 253 | id: realId, 254 | target 255 | }; 256 | const { data } = await axios.post(postUrl, postData); 257 | 258 | return data; 259 | }; 260 | 261 | /** 262 | * NOTE: MAH seems not work correctly with parent directory 263 | * @function makeDir 264 | * @param { object } config 265 | * @param { string } config.sessionKey 266 | * @param { string } config.host 267 | * @param { string | FileOrDir } config.id 268 | * @param { number } config.target 269 | * @param { string } config.directoryName 270 | * @param { boolean } config.isV1 271 | * @param { boolean } config.wsOnly 272 | */ 273 | const makeDir = async ({ 274 | sessionKey, 275 | host, 276 | id, 277 | target, 278 | directoryName, 279 | isV1, 280 | wsOnly, 281 | }) => { 282 | const realId = typeof id === 'string' ? id : id.id; 283 | if (wsOnly) return ws.file_mkdir({ 284 | id: realId, 285 | target, directoryName, 286 | }); 287 | if (isV1) return { code: 400, message: 'not supported' }; 288 | const postUrl = `${host}/file/mkdir`; 289 | const postData = { 290 | sessionKey, 291 | id: realId, 292 | target, 293 | directoryName, 294 | }; 295 | const { data } = await axios.post(postUrl, postData); 296 | return data; 297 | }; 298 | 299 | module.exports = { 300 | uploadFileAndSend, 301 | getGroupFileList, 302 | getGroupFileInfo, 303 | renameGroupFile, 304 | moveGroupFile, 305 | deleteGroupFile, 306 | makeDir, 307 | }; 308 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-mirai 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/node-mirai-sdk)](https://www.npmjs.com/package/node-mirai-sdk) 4 | 5 | [mirai](https://github.com/mamoe/mirai) 的 Node.js SDK. 6 | 7 | 由于还在开发中, 所有 API 均为待定. 8 | 9 | 最低支持: 10 | 11 | - `mirai-core-2.6.2` 12 | - `mirai-api-http-2.0.0` (部分兼容 `1.x`) 13 | 14 | ## QuickStart / 快速开始 15 | 16 | - 运行你的 [mirai-api-http](https://github.com/project-mirai/mirai-api-http) service 17 | - 安装 [node-mirai-sdk](https://www.npmjs.com/package/node-mirai-sdk) 18 | 19 | ```bash 20 | # for mirai-api-http 2.x 21 | npm i -S node-mirai-sdk@latest 22 | # for mirai-api-http 1.x 23 | npm i -S node-mirai-sdk@0.2.4 24 | ``` 25 | 26 | - 编写代码 (main.js) 27 | 28 | *PS: 注释中带 \* 为必须有。* 29 | 30 | ```javascript 31 | // es5 CommonJS 32 | const Mirai = require('node-mirai-sdk'); 33 | // es6 module or typescript 34 | import * as Mirai from 'node-mirai-sdk'; 35 | // typescript with `esModuleInterop: true` 36 | import Mirai from 'node-mirai-sdk'; 37 | 38 | const { Plain, At } = Mirai.MessageComponent; 39 | 40 | /** 41 | * 服务端设置(*) 42 | * host: mirai-api-http 的地址和端口,默认是 http://127.0.0.1:8080 43 | * verifyKey: mirai-api-http 的 verifyKey,建议手动指定 44 | * qq: 当前 BOT 对应的 QQ 号 45 | * enableWebsocket: 是否开启 WebSocket,需要和 mirai-api-http 的设置一致 46 | * wsOnly: 使用 WebSocket 发送数据 47 | */ 48 | const bot = new Mirai({ 49 | host: 'http://hostname:port', 50 | // mirai-api-http-2.x 51 | verifyKey: 'YourVerifyKey', 52 | // mirai-api-http-1.x 53 | authKey: 'YourAuthKey', 54 | qq: 123456, 55 | enableWebsocket: false, 56 | wsOnly: false, // 由于 WebSocket Adapter 不支持发送文件,部分功能仍需使用 HTTP Adapter 57 | }); 58 | 59 | // auth 认证(*) 60 | bot.onSignal('authed', () => { 61 | console.log(`Authed with session key ${bot.sessionKey}`); 62 | bot.verify(); 63 | }); 64 | 65 | // session 校验回调 66 | bot.onSignal('verified', async () => { 67 | console.log(`Verified with session key ${bot.sessionKey}`); 68 | 69 | // 获取好友列表,需要等待 session 校验之后 (verified) 才能调用 SDK 中的主动接口 70 | const friendList = await bot.getFriendList(); 71 | console.log(`There are ${friendList.length} friends in bot`); 72 | }); 73 | 74 | // 接受消息,发送消息(*) 75 | bot.onMessage(async message => { 76 | const { type, sender, messageChain, reply, quoteReply } = message; 77 | let msg = ''; 78 | messageChain.forEach(chain => { 79 | if (chain.type === 'Plain') 80 | msg += Plain.value(chain); // 从 messageChain 中提取文字内容 81 | }); 82 | 83 | // 直接回复 84 | if (msg.includes('收到了吗')) 85 | reply('收到了收到了'); // 或者: bot.reply('收到了收到了', message) 86 | // 引用回复 87 | else if (msg.includes('引用我')) 88 | quoteReply([At(sender.id), Plain('好的')]); // 或者: bot.quoteReply(messageChain, message) 89 | // 撤回消息 90 | else if (msg.includes('撤回')) 91 | bot.recall(message, sender.group.id); // 私聊为 sender.id 92 | // 发送图片,参数接受图片路径或 Buffer 93 | else if (msg.includes('来张图')) 94 | bot.sendImageMessage("./image.jpg", message); 95 | }); 96 | 97 | /* 开始监听消息(*) 98 | * 'all' - 监听好友和群 99 | * 'friend' - 只监听好友 100 | * 'group' - 只监听群 101 | * 'temp' - 只监听临时会话 102 | */ 103 | bot.listen('all'); // 相当于 bot.listen('friend', 'group', 'temp') 104 | 105 | // 退出前向 mirai-http-api 发送释放指令(*) 106 | process.on('exit', () => { 107 | bot.release(); 108 | }); 109 | ``` 110 | 111 | - 运行 BOT 112 | 113 | ```bash 114 | node main.js 115 | ``` 116 | 117 | ## 高级用法 118 | 119 | 另请参考 120 | 121 | - [事件订阅说明](https://github.com/RedBeanN/node-mirai/blob/master/event.md) 122 | - [API文档](https://redbean.tech/node-mirai-sdk) 123 | 124 | ```javascript 125 | const Mirai = require('node-mirai-sdk'); 126 | const { Plain, At, FlashImage, Image, Face, AtAll, Xml, Json, App, Poke, Forward } = Mirai.MessageComponent; 127 | const { Friend, Group } = Mirai.Target 128 | const { serialize, deserialize } = Mirai.MessageComponent; 129 | 130 | // ... 131 | 132 | bot.onMessage(async message => { 133 | const { type, sender, messageChain, reply, quoteReply, recall } = message; //接受其他消息,进行提取关键消息 134 | 135 | // 遍历消息链提取文本内容 136 | let msg = ''; 137 | messageChain.forEach(chain => { 138 | if (chain.type === 'Plain') msg += Plain.value(chain); //判断消息类型是不是文字 139 | }); 140 | 141 | // 将消息链序列化为含 mirai 码的字符串 142 | // 返回内容类似: 文字123[mirai:at:12345]文字234[mirai:image:{abcd}.png]文字345 143 | const serialized = serialize(messageChain0); 144 | 145 | // 把序列化后的字符串解析为 messageChain 146 | // [{ type: 'Plain', text: '文字123' }, { type: 'At', target: 12345, display: '' }, ...] 147 | const deserialized = deserialize(serialized); 148 | 149 | switch (msg) { 150 | case "文字测试": 151 | // 回复文字, 第一个参数可以是字符串或消息链, 第二个参数为原始消息或 `Mirai.Target` 构造的对象 152 | bot.reply('文字测试', message); 153 | bot.reply([Plain('文字测试')], Friend(123456)) 154 | // 可以使用从 `message` 解构的 `reply` 方法, 无需传入原始消息 155 | reply('文字测试'); // message.reply('文字测试') 156 | break; 157 | 158 | case "撤回测试": 159 | // 撤回指定的消息 160 | // 从 MAH 2.6.0 开始, 如果`message`不带有`sender`信息则需传入原始 QQ 或群号 161 | // 撤回其他群员的消息需要 bot 有管理权限, 管理员不能撤回群主的消息 162 | bot.recall(message, sender.group.id); 163 | // 可以使用从 `message` 解构的 `recall` 方法, 无需传入原始消息 164 | recall(); // message.recall() 165 | break; 166 | // 或者 167 | case "撤回测试2": 168 | // 发送消息的接口返回值均提供 `recall` 方法以供撤回, 无需传入原始 QQ 或群号 169 | const res = await reply('这条消息将被撤回'); // bot.recall(res) throws error! 170 | setTimeout(() => res.recall(), 2000); 171 | break; 172 | 173 | case "图片测试": 174 | bot.reply([ 175 | Image({ 176 | url: 'https://i2.hdslb.com/bfs/archive/68662ffb133c15232d4c7e763c43e07bccc98ccb.jpg' 177 | }) 178 | ], message); // 回复图片 179 | break; 180 | 181 | case "表情测试": 182 | bot.reply([Face(123)], message); // 回复表情 183 | break; 184 | 185 | case "闪照测试": 186 | bot.reply([ 187 | FlashImage({ 188 | url:'https://i2.hdslb.com/bfs/archive/68662ffb133c15232d4c7e763c43e07bccc98ccb.jpg' 189 | }) 190 | ], message); // 回复闪照 191 | break; 192 | 193 | case "引用测试": 194 | bot.quoteReply([Plain('引用测试')], message); // 引用消息 195 | break; 196 | 197 | case "@测试": 198 | bot.reply([At(sender.id)], message); // @测试 199 | break; 200 | 201 | case "全体@测试": 202 | bot.reply([AtAll()], message); // 全体@测试" 203 | break; 204 | 205 | case "戳一戳测试": 206 | bot.reply([Poke('Poke')], message); // 戳一戳 207 | break; 208 | 209 | case "转发测试": 210 | const forwardMessage = [Forward([ 211 | message, // 可以直接转发消息本体 212 | message.messageChain.find(c => c.type === 'Source').id, // 也可以引用源消息ID 213 | { 214 | senderId: 10001, // QQ号 215 | senderName: 'Pony', // 显示名称 216 | time: Math.floor(Date.now() / 1000), // 时间戳以秒为单位 217 | messageChain: [Plain('用_创造快乐')] 218 | } // 或者自行构建消息列表 219 | ])]; // 三者可以混合使用 220 | reply(forwardMessage); // 转发消息 221 | break; 222 | 223 | case "全体禁言测试": 224 | bot.setGroupMuteAll(sender.group.id, message); // 全体禁言 225 | break; 226 | 227 | case "全体禁言取消测试": 228 | bot.setGroupUnmuteAll(sender.group.id, message); // 全体禁言取消 229 | break; 230 | 231 | case "禁言群员测试": 232 | bot.setGroupMute(sender.group.id,sender.id); // 禁言群成员10分钟 233 | break; 234 | 235 | case "解除禁言群员测试": 236 | bot.setGroupUnmute(sender.group.id,sender.id); // 解除群成员禁言 237 | break; 238 | 239 | case "移除群成员测试": 240 | bot.setGroupKick(sender.group.id,sender.id); // 移除群成员 241 | break; 242 | 243 | case "发布群公告测试": 244 | bot.setGroupConfig(sender.group.id, { 245 | announcement:'这是一个测试公告' 246 | }); // 发布群公告 247 | break; 248 | 249 | case "修改群员资料测试": 250 | bot.setGroupMemberInfo(sender.group.id, sender.id, { 251 | name: '测试' 252 | }); // 修改群员资料 253 | break; 254 | } 255 | }); 256 | 257 | ``` 258 | 259 | ## TypeScript 支持(实验性) 260 | 261 | `types/index.d.ts` 为 `tsc` 自动生成, 部分枚举类型可通过 `node-mirai-sdk/type` 引入 262 | 263 | ``` typescript 264 | import { events, Permission } from 'node-mirai-sdk/type' 265 | // ... 266 | bot.onEvent(events.groupPermissionChange, (event) => { 267 | if (event.Permission === Permission.MEMBER) { 268 | // do something 269 | } 270 | }) 271 | 272 | ``` 273 | 274 | ## 使用 `target` 构造 275 | 276 | (由 [@kirainmoe](https://github.com/kirainmoe) 提供) 277 | 278 | `Bot` 主动调用部分接口, 需要按照消息的格式, 构造发送对象 `target`: 279 | 280 | ```javascript 281 | // 私聊发送图片给 12345678 282 | bot.sendImageMessage(url, { type: 'FriendMessage', sender: { id: 12345678 } }); 283 | // 群聊发送图片到 998244353 284 | bot.sendImageMessage(url, { type: 'FriendMessage', sender: { group: { id: 998244353 } } }); 285 | 286 | ``` 287 | 288 | 通过从 NodeMirai.Target 引入 Friend, Group 或 Temp 可以省去构造 target 的过程: 289 | 290 | ```javascript 291 | const { Friend, Group, Temp } = require("node-mirai-sdk").Target; 292 | bot.sendImageMessage(url, Friend(12345678)); 293 | bot.sendImageMessage(url, Group(998244353)); 294 | bot.sendImageMessage(url, Temp(998244353, 12345678)); // 给群号为 998244353 的用户 12345678 发送临时消息图片 295 | 296 | ``` 297 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import { Permission, GroupPermissionInfo, GroupMember, MessageChain, MessageType, message, httpApiResponse } from "./typedef" 2 | 3 | export enum events { 4 | online = "online", 5 | offlineActive = "offlineActive", 6 | offlineForce = "offlineForce", 7 | offlineDropped = "offlineDropped", 8 | relogin = "relogin", 9 | groupPermissionChange = "groupPermissionChange", 10 | mute = "mute", 11 | unmute = "unmute", 12 | leaveActive = "leaveActive", 13 | leaveKick = "leaveKick", 14 | leaveDisband = "leaveDisband", 15 | joinGroup = "joinGroup", 16 | invitedJoinGroupRequest = "invitedJoinGroupRequest", 17 | groupNameChange = "groupNameChange", 18 | groupEntranceAnnouncementChange = "groupEntranceAnnouncementChange", 19 | groupMuteAll = "groupMuteAll", 20 | groupAllowAnonymousChat = "groupAllowAnonymousChat", 21 | groupAllowConfessTalk = "groupAllowConfessTalk", 22 | groupAllowMemberInvite = "groupAllowMemberInvite", 23 | groupRecall = "groupRecall", 24 | friendRecall = "friendRecall", 25 | friendNickChanged = "friendNickChanged", 26 | friendInputStatusChanged = "friendInputStatusChanged", 27 | memberJoin = "memberJoin", 28 | memberLeaveKick = "memberLeaveKick", 29 | memberLeaveQuit = "memberLeaveQuit", 30 | memberCardChange = "memberCardChange", 31 | memberSpecialTitleChange = "memberSpecialTitleChange", 32 | memberPermissionChange = "memberPermissionChange", 33 | memberMute = "memberMute", 34 | memberUnmute = "memberUnmute", 35 | memberHonorChange = "memberHonorChange", 36 | memberJoinRequest = "memberJoinRequest", 37 | newFriendRequest = "newFriendRequest", 38 | nudge = "nudge", 39 | otherClientOnline = "otherClientOnline", 40 | otherClientOffline = "otherClientOffline", 41 | commandExecuted = "commandExecuted", 42 | } 43 | 44 | interface online { 45 | type: 'BotOnlineEvent', 46 | qq: number, 47 | } 48 | interface offlineActive { 49 | type: 'BotOfflineEventActive', 50 | qq: number, 51 | } 52 | interface offlineForce { 53 | type: 'BotOfflineEventForce', 54 | qq: number, 55 | } 56 | interface offlineDropped { 57 | type: 'BotOfflineEventDropped', 58 | qq: number, 59 | } 60 | interface relogin { 61 | type: 'BotReloginEvent', 62 | qq: number, 63 | } 64 | 65 | interface Friend { 66 | id: number, 67 | nickname: string, 68 | remark: string, 69 | } 70 | interface friendInputStatusChanged { 71 | type: 'FriendInputStatusChangedEvent', 72 | friend: Friend, 73 | inputting: boolean, 74 | } 75 | interface friendNickChanged { 76 | type: 'FriendNickChangedEvent', 77 | friend: Friend, 78 | from: string, 79 | to: string, 80 | } 81 | 82 | interface MemberWithPermission extends GroupMember { 83 | group: GroupPermissionInfo, 84 | } 85 | interface groupPermissionChange { 86 | type: 'BotGroupPermissionChangeEvent', 87 | origin: Permission, 88 | current: Permission, 89 | group: GroupPermissionInfo, 90 | } 91 | interface mute { 92 | type: 'BotMuteEvent', 93 | durationSeconds: number, 94 | operator: MemberWithPermission, 95 | } 96 | interface unmute { 97 | type: 'BotUnmuteEvent', 98 | operator: MemberWithPermission, 99 | } 100 | interface groupMuteAll { 101 | type: 'GroupMuteAllEvent', 102 | origin: boolean, 103 | current: boolean, 104 | group: GroupPermissionInfo, 105 | operator: MemberWithPermission, 106 | } 107 | interface joinGroup { 108 | type: 'BotJoinGroupEvent', 109 | group: GroupPermissionInfo, 110 | invitor?: GroupMember | null, 111 | } 112 | interface leaveActive { 113 | type: 'BotLeaveEventActive', 114 | group: GroupPermissionInfo, 115 | } 116 | interface leaveKick { 117 | type: 'BotLeaveEventKick', 118 | group: GroupPermissionInfo, 119 | operator: MemberWithPermission, 120 | } 121 | interface leaveDisband { 122 | type: 'BotLeaveEventDisband', 123 | group: GroupPermissionInfo 124 | } 125 | interface groupNameChange { 126 | type: 'GroupNameChangeEvent', 127 | origin: string, 128 | current: string, 129 | group: GroupPermissionInfo, 130 | operator: MemberWithPermission, 131 | } 132 | interface groupEntranceAnnouncementChange { 133 | type: 'GroupEntranceAnnouncementChangeEvent', 134 | origin: string, 135 | current: string, 136 | group: GroupPermissionInfo, 137 | operator: MemberWithPermission 138 | } 139 | interface groupRecall { 140 | type: 'GroupRecallEvent', 141 | authorId: number, 142 | messageId: number, 143 | time: number, 144 | group: GroupPermissionInfo, 145 | operator: MemberWithPermission, 146 | } 147 | interface friendRecall { 148 | type: 'FriendRecallEvent', 149 | authorId: number, 150 | messageId: number, 151 | time: number, 152 | operator: number, 153 | } 154 | interface groupAllowAnonymousChat { 155 | type: 'GroupAllowAnonymousChatEvent', 156 | origin: boolean, 157 | current: boolean, 158 | group: GroupPermissionInfo, 159 | operator: MemberWithPermission, 160 | } 161 | interface groupAllowConfessTalk { 162 | type: 'GroupAllowConfessTalkEvent', 163 | origin: boolean, 164 | current: boolean, 165 | group: GroupPermissionInfo, 166 | isByBot: boolean, 167 | } 168 | interface groupAllowMemberInvite { 169 | type: 'GroupAllowMemberInviteEvent', 170 | origin: boolean, 171 | current: boolean, 172 | group: GroupPermissionInfo, 173 | operator: MemberWithPermission, 174 | } 175 | interface memberJoin { 176 | type: 'MemberJoinEvent', 177 | member: MemberWithPermission, 178 | invitor: GroupMember | null, 179 | } 180 | interface memberLeaveKick { 181 | type: 'MemberLeaveEventKick', 182 | member: MemberWithPermission, 183 | operator: MemberWithPermission, 184 | } 185 | interface memberLeaveQuit { 186 | type: 'MemberLeaveEventQuit', 187 | member: MemberWithPermission, 188 | } 189 | interface memberCardChange { 190 | type: 'MemberCardChangeEvent', 191 | origin: string, 192 | current: string, 193 | member: MemberWithPermission, 194 | } 195 | interface memberSpecialTitleChange { 196 | type: 'MemberSpecialTitleChangeEvent', 197 | origin: string, 198 | current: string, 199 | member: MemberWithPermission, 200 | } 201 | interface memberPermissionChange { 202 | type: 'MemberPermissionChangeEvent', 203 | origin: Permission, 204 | current: Permission, 205 | member: MemberWithPermission, 206 | } 207 | interface memberMute { 208 | type: 'MemberMuteEvent', 209 | durationSeconds: number, 210 | member: MemberWithPermission, 211 | operator: MemberWithPermission, 212 | } 213 | interface memberUnmute { 214 | type: 'MemberUnmuteEvent', 215 | member: MemberWithPermission, 216 | operator: MemberWithPermission, 217 | } 218 | interface memberHonorChange { 219 | type: 'MemberHonorChangeEvent', 220 | action: 'achieve' | 'lose', 221 | honor: string, 222 | member: MemberWithPermission, 223 | } 224 | 225 | interface newFriendRequest { 226 | type: 'NewFriendRequestEvent', 227 | eventId: number, 228 | fromId: number, 229 | groupId: number, 230 | nick: string, 231 | message: string, 232 | /** 233 | * 接受好友申请 234 | */ 235 | accept: (msg?: string) => Promise, 236 | /** 237 | * 拒绝好友申请 238 | */ 239 | reject: (msg?: string) => Promise, 240 | /** 241 | * 拒绝并拉黑, 不再接收该用户的好友申请 242 | */ 243 | rejectAndBlock: (msg?: string) => Promise, 244 | } 245 | interface memberJoinRequest { 246 | type: 'MemberJoinRequestEvent', 247 | eventId: number, 248 | fromId: number, 249 | groupId: number, 250 | groupName: string, 251 | nick: string, 252 | message: string, 253 | /** 254 | * 接受加群申请 255 | */ 256 | accept: (msg?: string) => Promise, 257 | /** 258 | * 拒绝加群申请 259 | */ 260 | reject: (msg?: string) => Promise, 261 | /** 262 | * 忽略加群申请 263 | */ 264 | ignore: (msg?: string) => Promise, 265 | /** 266 | * 拒绝并拉黑 267 | */ 268 | rejectAndBlock: (msg?: string) => Promise, 269 | /** 270 | * 忽略并拉黑 271 | */ 272 | ignoreAndBlock: (msg?: string) => Promise, 273 | } 274 | interface invitedJoinGroupRequest { 275 | type: 'BotInvitedJoinGroupRequestEvent', 276 | eventId: number, 277 | fromId: number, 278 | groupId: number, 279 | groupName: string, 280 | nick: string, 281 | message: string, 282 | /** 283 | * 接受邀请 284 | */ 285 | accept: (msg?: string) => Promise, 286 | /** 287 | * 拒绝邀请 288 | */ 289 | reject: (msg?: string) => Promise, 290 | } 291 | interface otherClientOnline { 292 | type: 'OtherClientOnlineEvent', 293 | client: { 294 | id: number, 295 | platform: string, 296 | }, 297 | kind: number, 298 | } 299 | interface otherClientOffline { 300 | type: 'OtherClientOfflineEvent', 301 | client: { 302 | id: number, 303 | platform: string, 304 | }, 305 | } 306 | 307 | interface nudge { 308 | type: 'NudgeEvent', 309 | fromId: number, 310 | subject: { 311 | id: number, 312 | kind: 'Friend' | 'Group', 313 | }, 314 | action: string, 315 | suffix: string, 316 | target: number, 317 | } 318 | 319 | interface commandExecuted { 320 | type: 'CommandExecutedEvent', 321 | name: string, 322 | friend: Friend | null, 323 | member: GroupMember | null, 324 | args: Array, 325 | } 326 | 327 | export interface EventMap { 328 | online: online, 329 | offlineActive: offlineActive, 330 | offlineForce: offlineForce, 331 | offlineDropped: offlineDropped, 332 | relogin: relogin, 333 | groupPermissionChange: groupPermissionChange, 334 | mute: mute, 335 | unmute: unmute, 336 | leaveActive: leaveActive, 337 | leaveKick: leaveKick, 338 | leaveDisband: leaveDisband, 339 | joinGroup: joinGroup, 340 | invitedJoinGroupRequest: invitedJoinGroupRequest, 341 | groupNameChange: groupNameChange, 342 | groupEntranceAnnouncementChange: groupEntranceAnnouncementChange, 343 | groupMuteAll: groupMuteAll, 344 | groupAllowAnonymousChat: groupAllowAnonymousChat, 345 | groupAllowConfessTalk: groupAllowConfessTalk, 346 | groupAllowMemberInvite: groupAllowMemberInvite, 347 | groupRecall: groupRecall, 348 | friendRecall: friendRecall, 349 | friendNickChanged: friendNickChanged, 350 | friendInputStatusChanged: friendInputStatusChanged, 351 | memberJoin: memberJoin, 352 | memberLeaveKick: memberLeaveKick, 353 | memberLeaveQuit: memberLeaveQuit, 354 | memberCardChange: memberCardChange, 355 | memberSpecialTitleChange: memberSpecialTitleChange, 356 | memberPermissionChange: memberPermissionChange, 357 | memberMute: memberMute, 358 | memberUnmute: memberUnmute, 359 | memberHonorChange: memberHonorChange, 360 | memberJoinRequest: memberJoinRequest, 361 | newFriendRequest: newFriendRequest, 362 | nudge: nudge, 363 | otherClientOnline: otherClientOnline, 364 | otherClientOffline: otherClientOffline, 365 | commandExecuted: commandExecuted, 366 | } 367 | 368 | export interface AllEventMap extends EventMap { 369 | message: message, 370 | authed: void, 371 | verified: void, 372 | released: void, 373 | } -------------------------------------------------------------------------------- /src/typedef.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file typedef.ts 3 | * @description Define types for .d.ts 4 | */ 5 | /** 6 | * @description 来自 MiraiApiHttp 的响应状态码 7 | */ 8 | export enum ResponseCode { 9 | SUCCESS = 0, 10 | VERIFY_KEY_ERROR = 1, 11 | BOT_NOT_EXIST = 2, 12 | SESSION_INVALID = 3, 13 | SESSION_NOT_AUTH = 4, 14 | TARGET_NOT_EXIST = 5, 15 | FILE_NOT_EXIST = 6, 16 | NOT_PERMISSION = 10, 17 | BOT_MUTED = 20, 18 | MESSAGE_TOO_LONG = 30, 19 | PARAM_ERROR = 400, 20 | } 21 | /** 22 | * @description 群员或 bot 在群内的权限 23 | */ 24 | export enum Permission { 25 | OWNER = 'OWNER', 26 | ADMINISTRATOR = 'ADMINISTRATOR', 27 | MEMBER = 'MEMBER', 28 | } 29 | /** 30 | * @description 消息类型 31 | */ 32 | export enum MessageType { 33 | FriendMessage = 'FriendMessage', 34 | GroupMessage = 'GroupMessage', 35 | TempMessage = 'TempMessage', 36 | // TODO: Support following types 37 | StrangerMessage = 'StrangerMessage', 38 | OtherClientMessage = 'OtherClientMessage', 39 | FriendSyncMessage = 'FriendSyncMessage', 40 | GroupSyncMessage = 'GroupSyncMessage', 41 | TempSyncMessage = 'TempSyncMessage', 42 | StrangerSyncMessage = 'StrangerSyncMessage', 43 | } 44 | /** 45 | * 好友资料 - 性别 46 | */ 47 | export enum UserSex { 48 | UNKNOWN = 'UNKNOWN', 49 | MALE = 'MALE', 50 | FEMALE = 'FEMALE', 51 | } 52 | 53 | export enum ChainType { 54 | Source = 'Source', 55 | Plain = 'Plain', 56 | At = 'At', 57 | AtAll = 'AtAll', 58 | Face = 'Face', 59 | Image = 'Image', 60 | FlashImage = 'FlashImage', 61 | Xml = 'Xml', 62 | Json = 'Json', 63 | App = 'App', 64 | Quote = 'Quote', 65 | Poke = 'Poke', 66 | Dice = 'Dice', 67 | Voice = 'Voice', 68 | Audio = 'Audio', 69 | MarketFace = 'MarketFace', 70 | MusicShare = 'MusicShare', 71 | ForwardMessage = 'Forward', 72 | File = 'File', 73 | MiraiCode = 'MiraiCode', 74 | } 75 | 76 | /** 77 | * @typedef { Object } GroupMember 群成员 78 | * @property { number } id QQ 79 | * @property { string } memberName 群昵称 80 | * @property { 'OWNER' | 'ADMINISTRATOR' | 'MEMBER' } permission 群员在群中的权限 81 | * @property { string } specialTitle 群头衔 82 | * @property { number } joinTimestamp 加群时间 83 | * @property { number } lastSpeakTimestamp 最近发言时间 84 | * @property { number } muteTimeRemaining 禁言剩余时间 85 | * @property { GroupPermissionInfo } group bot在群中的权限信息 86 | */ 87 | export type GroupMember = { 88 | /** 89 | * QQ 号 90 | */ 91 | id: number, 92 | /** 93 | * 群昵称 94 | */ 95 | memberName: string, 96 | /** 97 | * 群权限 98 | */ 99 | permission: Permission, 100 | /** 101 | * 群头衔 102 | */ 103 | specialTitle: string, 104 | /** 105 | * 加群时间 106 | */ 107 | joinTimestamp: number, 108 | /** 109 | * 最后发言时间 110 | */ 111 | lastSpeakTimestamp: number, 112 | /** 113 | * 禁言剩余时间 114 | */ 115 | muteTimeRemaining: number, 116 | } 117 | /** 118 | * @typedef { Object } GroupInfo 群资料 119 | * @property { number } id 群号 120 | * @property { string } name 群名 121 | * @property { string } announcement 群公告 122 | * @property { boolean } confessTalk 是否开启坦白说 123 | * @property { boolean } allowMemberInvite 是否允许群员邀请 124 | * @property { boolean } autoAprove 是否开启自动审批 125 | * @property { boolean } anonymousChat 是否开启匿名聊天 126 | */ 127 | export type GroupInfo = { 128 | /** 129 | * 群号 130 | */ 131 | id: number, 132 | /** 133 | * 群名 134 | */ 135 | name: string, 136 | /** 137 | * 群公告 138 | */ 139 | announcement: string, 140 | /** 141 | * 是否开启坦白说 142 | */ 143 | confessTalk: boolean, 144 | /** 145 | * 是否允许群员邀请 146 | */ 147 | allowMemberInvite: boolean, 148 | /** 149 | * 是否开启自动审批 150 | */ 151 | autoAprove: boolean, 152 | /** 153 | * 是否开启匿名聊天 154 | */ 155 | anonymousChat: boolean, 156 | } 157 | 158 | /** 159 | * @typedef { Object } GroupFile 群文件 160 | * @property { string } name 文件名 161 | * @property { string } id 文件ID 162 | * @property { string } path 文件路径 163 | * @property { GroupFile | null } parent 所在文件夹, null为根目录 164 | * @property { GroupPermissionInfo } contact 群权限信息 165 | * @property { boolean } isFile 是否为文件 166 | * @property { boolean } isDirectory 是否为文件夹 167 | * @property { object } [downloadInfo] 下载信息 168 | * @property { string } downloadInfo.sha1 sha1校验 169 | * @property { string } downloadInfo.md5 md5校验 170 | * @property { number } downloadInfo.downloadTimes 下载次数 171 | * @property { number } downloadInfo.uploaderId 上传者 172 | * @property { number } downloadInfo.uploadTime 上传时间 173 | * @property { number } downloadInfo.lastModifyTime 更新时间 174 | * @property { string } downloadInfo.url 下载url 175 | */ 176 | export type GroupFile = { 177 | /** 178 | * 文件名 179 | */ 180 | name: string, 181 | /** 182 | * 文件ID 183 | */ 184 | id: string, 185 | /** 186 | * 文件路径 187 | */ 188 | path: string, 189 | /** 190 | * 所在文件夹, null为根目录 191 | */ 192 | parent: GroupFile | null, 193 | /** 194 | * 群权限信息 195 | */ 196 | contact: GroupPermissionInfo, 197 | /** 198 | * 是否为文件 199 | */ 200 | isFile: boolean, 201 | /** 202 | * 是否为文件夹 203 | */ 204 | isDirectory: boolean, 205 | downloadInfo?: { 206 | /** 207 | * SHA1 校验 208 | */ 209 | sha1: string, 210 | /** 211 | * MD5 校验 212 | */ 213 | md5: string, 214 | /** 215 | * 下载次数 216 | */ 217 | downloadTimes: number, 218 | /** 219 | * 上传者 220 | */ 221 | uploaderId: number, 222 | /** 223 | * 上传时间 224 | */ 225 | uploadTime: number, 226 | /** 227 | * 更新时间 228 | */ 229 | lastModifyTime: number, 230 | /** 231 | * 下载 URL 232 | */ 233 | url: string, 234 | } 235 | } 236 | 237 | /** 238 | * @typedef { Object } GroupPermissionInfo 群权限信息 239 | * @property { number } id 群号 240 | * @property { string } name 群名 241 | * @property { 'OWNER' | 'ADMINISTRATOR' | 'MEMBER' } permission bot 在群内的权限 242 | */ 243 | export type GroupPermissionInfo = { 244 | id: number, 245 | name: string, 246 | /** 247 | * Bot 在群内的权限 248 | */ 249 | permission: Permission, 250 | } 251 | /** 252 | * @typedef { Object } Friend 253 | * @property { number } id 发送者的QQ 254 | * @property { string } nickname 发送者的昵称 255 | * @property { string } remark 发送者的备注 256 | */ 257 | export type Friend = { 258 | id: number, 259 | nickname: string, 260 | remark: string, 261 | } 262 | export type GroupSender = { 263 | id: number, 264 | memberName: string, 265 | specialTitle: string, 266 | permission: Permission, 267 | joinTimestamp: number, 268 | lastSpeakTimestamp: number, 269 | group: GroupPermissionInfo, 270 | } 271 | 272 | 273 | /** 274 | * @typedef { Object } httpApiResponse 275 | * @property { number } code 状态码 https://github.com/project-mirai/mirai-api-http/blob/master/docs/api/API.md#%E7%8A%B6%E6%80%81%E7%A0%81 276 | * @property { string } msg response message 277 | * @property { string } [session] auth(verify) 响应 278 | * @property { number } [messageId] reply/quoteReply 响应,标识本条消息,用于撤回和引用回复 279 | */ 280 | export type httpApiResponse = { 281 | code: ResponseCode, 282 | msg: string, 283 | session?: string, 284 | messageId?: string, 285 | } 286 | 287 | export type MessageResponse = httpApiResponse & { 288 | /** 289 | * 撤回发出的消息 290 | */ 291 | recall: () => Promise 292 | } 293 | 294 | /** 295 | * @typedef { Object } ForwardNode 296 | * @property { number } senderId 发送人QQ 297 | * @property { number } time 发送时间 298 | * @property { string } senderName 显示名称 299 | * @property { MessageChain[] } [messageChain] 300 | */ 301 | export type ForwardNode = { 302 | senderId: number, 303 | time: number, 304 | senderName: string, 305 | messageChain: MessageChain[] 306 | } 307 | /** 308 | * @typedef { Array } ForwardNodeList 309 | */ 310 | export type ForwardNodeList = Array 311 | 312 | /** 313 | * @typedef { Object } MessageChain 消息链对象, node-mirai-sdk 提供各类型的 .value() 方法获得各自的属性值 314 | * @property { string } type 消息类型 315 | * @property { number | string } [id] Source 类型中的消息 id, 或Quote类型中引用的源消息的 id, 或文件 id 316 | * @property { number } [time] Source 类型中的时间戳 317 | * @property { number } [groupId] Quote 类型中源消息所在群的群号, 好友消息时为 0 318 | * @property { number } [senderId] Quote 类型中源消息发送者的 qq 号 319 | * @property { object } [origin] Quote 类型中源消息的 MessageChain 320 | * @property { string } [text] Plain 类型的文本 321 | * @property { number } [target] At 类型中 @ 目标的 qq 号 322 | * @property { string } [display] At 类型中 @ 目标的群名片 323 | * @property { number } [faceId] Face 类型中表情的编号 324 | * @property { string } [imageId] Image/FlashImage 类型中图片的 imageId 325 | * @property { string } [voiceId] Voice 类型中语音的 voiceId 326 | * @property { string } [url] Image/FlashImage/Voice 类型中图片的 url, 可用于下载图片和语音 327 | * @property { string } [xml] Xml 类型中的 xml 字符串 328 | * @property { string } [json] Json 类型中的 json 字符串 329 | * @property { string } [content] App 类型中的 app content 字符串 330 | * @property { string } [name] Poke/File 类型中的 name 331 | * @property { number } [size] File 类型中的 size 332 | * @property { number } [value] Dice 类型中的骰子点数 333 | * @property { 'QQMusic'|'NeteaseCloudMusic'|'MiguMusic'|'KugouMusic'|'KuwoMusic' } [kind] MusicShare - 类型 334 | * @property { string } [title] MusicShare - 标题 335 | * @property { string } [summary] MusicShare - 概括 336 | * @property { string } [jumpUrl] MusicShare - 跳转路径 337 | * @property { string } [pictureUrl] MusicShare - 封面路径 338 | * @property { string } [musicUrl] MusicShare - 音源路径 339 | * @property { string } [brief] MusicShare - 简介 340 | * @property { string } [code] MiraiCode 341 | * @property { ForwardNodeList } [nodeList] ForwardMessage - 转发消息列表 342 | */ 343 | export type MessageChain = { 344 | type: ChainType, 345 | /** 346 | * Source 类型中的消息 id, 或Quote类型中引用的源消息的 id, 或文件 id 347 | */ 348 | id?: number, 349 | /** 350 | * Source 类型中的时间戳 351 | */ 352 | time?: number, 353 | /** 354 | * Quote 类型中源消息所在群的群号, 好友消息时为 0 355 | */ 356 | groupId?: number, 357 | /** 358 | * Quote 类型中源消息发送者的 qq 号 359 | */ 360 | senderId?: number, 361 | /** 362 | * Quote 类型中源消息的 MessageChain 363 | */ 364 | origin?: MessageChain, 365 | /** 366 | * Plain 类型的文本 367 | */ 368 | text?: string, 369 | /** 370 | * At 类型中 @ 目标的 qq 号 371 | */ 372 | target?: number, 373 | /** 374 | * At 类型中 @ 目标的群名片 375 | */ 376 | display?: string, 377 | /** 378 | * Face 类型中表情的编号 379 | */ 380 | faceId?: number, 381 | /** 382 | * Image/FlashImage 类型中图片的 imageId 383 | */ 384 | imageId?: string, 385 | /** 386 | * Image/FlashImage 类型中图片的宽度 387 | */ 388 | width: number, 389 | /** 390 | * Image/FlashImage 类型中图片的高度 391 | */ 392 | height: number, 393 | /** 394 | * Image/FlashImage 类型中图片是否为表情 395 | */ 396 | isEmoji: boolean, 397 | /** 398 | * Image/FlashImage 类型中图片的大小 / File 类型中的 size 399 | */ 400 | size?: number, 401 | /** 402 | * Image/FlashImage 类型中图片的类型(UpperCase) 403 | */ 404 | imageType: 'JPG' | 'PNG' | 'WEBP' | 'BMP' | 'GIG' | 'APNG' | 'SHARPP', 405 | /** 406 | * Voice 类型中语音的 voiceId 407 | */ 408 | voiceId?: string, 409 | /** 410 | * Image/FlashImage/Voice 类型中图片的 url, 可用于下载图片和语音 411 | */ 412 | url?: string, 413 | /** 414 | * Xml 类型中的 xml 字符串 415 | */ 416 | xml?: string, 417 | /** 418 | * Json 类型中的 json 字符串 419 | */ 420 | json?: string, 421 | /** 422 | * App 类型中的 app content 字符串 423 | */ 424 | content?: string, 425 | /** 426 | * Poke/File 类型中的 name 427 | */ 428 | name?: string, 429 | /** 430 | * Dice 类型中的骰子点数 431 | */ 432 | value?: number, 433 | /** 434 | * MusicShare - 类型 435 | */ 436 | kind?: 'QQMusic'|'NeteaseCloudMusic'|'MiguMusic'|'KugouMusic'|'KuwoMusic', 437 | /** 438 | * MusicShare - 标题 439 | */ 440 | title?: string, 441 | /** 442 | * MusicShare - 概括 443 | */ 444 | summary?: string, 445 | /** 446 | * MusicShare - 跳转路径 447 | */ 448 | jumpUrl?: string, 449 | /** 450 | * MusicShare - 封面路径 451 | */ 452 | pictureUrl?: string, 453 | /** 454 | * MusicShare - 音源路径 455 | */ 456 | musicUrl?: string, 457 | /** 458 | * MusicShare - 简介 459 | */ 460 | brief?: string, 461 | code?: string, 462 | /** 463 | * Forward - 消息列表 464 | */ 465 | nodeList: ForwardNodeList, 466 | } 467 | /** 468 | * @callback replyFunction 469 | * @param { string | MessageChain[] } message 回复的消息 470 | * @returns { Promise } 471 | */ 472 | declare function replyFunction(message: string | MessageChain[]): Promise; 473 | /** 474 | * @callback recallFunction 475 | * @param { string | MessageChain[] } message 回复的消息 476 | * @returns { Promise } 477 | */ 478 | declare function recallFunction(message: string | MessageChain[]): Promise; 479 | 480 | /** 481 | * @typedef { Object } message 消息 482 | * @property { "FriendMessage"|"GroupMessage"|"TempMessage" } type 消息类型 483 | * @property { MessageChain[] } messageChain 消息链对象 484 | * @property { Sender } sender 发送者 485 | * @property { replyFunction } reply 快速回复消息 486 | * @property { replyFunction } quoteReply 快速引用回复消息 487 | * @property { recallFunction } recall 撤回此条消息 488 | */ 489 | export type message = { 490 | type: MessageType, 491 | messageChain: MessageChain[], 492 | sender: Friend | GroupSender, 493 | /** 494 | * 快速回复消息 495 | */ 496 | reply: typeof replyFunction, 497 | /** 498 | * 快速引用回复消息 499 | */ 500 | quoteReply: typeof replyFunction, 501 | /** 502 | * 撤回此条消息 503 | */ 504 | recall: typeof recallFunction, 505 | } 506 | 507 | /** 508 | * @typedef { Object } UserInfo 用户资料 509 | * @property { string } nickname 用户名 510 | * @property { string } email 邮箱 511 | * @property { number } age 年龄 512 | * @property { number } level 等级 513 | * @property { string } sign 签名 514 | * @property { 'UNKNOWN' | 'MALE' | 'FEMALE' } sex 性别 515 | */ 516 | export type UserInfo = { 517 | nickname: string, 518 | email: string, 519 | age: number, 520 | level: number, 521 | sign: string, 522 | sex: UserSex, 523 | } 524 | 525 | // these seems not working well with tsc, just ignore its warning 526 | // @ts-ignore 527 | export type Buffer = import('buffer').Buffer 528 | // @ts-ignore 529 | export type ReadStream = import('fs').ReadStream 530 | // @ts-ignore 531 | export type WebSocket = import('ws') -------------------------------------------------------------------------------- /src/MessageComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { import('./typedef').MessageChain } MessageChain 3 | * @typedef { import('./typedef').message } message 4 | * 5 | * @typedef { Object } source 6 | * @property { number } id 7 | * @property { number } time 8 | * 9 | * @typedef { Object } at 10 | * @property { number } target 11 | * @property { string } [display] 12 | * 13 | * @typedef { Object } image 14 | * @property { string } [imageId] 15 | * @property { string } [url] 16 | * 17 | * @typedef { Object } voice 18 | * @property { string } voiceId 19 | * @property { string } [url] 20 | * 21 | * @typedef { Object } quote 22 | * @property { number } id 23 | * @property { number } [groupId] 24 | * @property { number } [senderId] 25 | * @property { MessageChain[] } [origin] 26 | * 27 | * @typedef { Object } music 28 | * @property { 'QQMusic'|'NeteaseCloudMusic'|'MiguMusic'|'KugouMusic'|'KuwoMusic' } kind 29 | * @property { string } title 30 | * @property { string } summary 31 | * @property { string } jumpUrl 32 | * @property { string } pictureUrl 33 | * @property { string } musicUrl 34 | * @property { string } [brief] 35 | * 36 | * @typedef { Object } ForwardNode 37 | * @property { number } senderId 38 | * @property { number } time 39 | * @property { string } senderName 40 | * @property { MessageChain[] } [messageChain] 41 | * @property { number } [messageId] 42 | * @typedef { Array } nodeList 43 | */ 44 | 45 | /** 46 | * @function Source 47 | * @param { number } id 48 | * @returns { MessageChain } 49 | */ 50 | const Source = id => { 51 | return { 52 | type: 'Source', 53 | id, 54 | }; 55 | }; 56 | /** 57 | * @method Source#value 58 | * @param { MessageChain } source 59 | * @returns { source } 60 | */ 61 | Source.value = source => { 62 | return { 63 | id: source.id, 64 | time: source.time, 65 | }; 66 | }; 67 | 68 | /** 69 | * @function Plain 70 | * @param { string } text 71 | * @returns { MessageChain } 72 | */ 73 | const Plain = text => { 74 | return { 75 | type: 'Plain', 76 | text, 77 | }; 78 | }; 79 | /** 80 | * @method Plain#value 81 | * @param { MessageChain } plain 82 | * @returns { string } 83 | */ 84 | Plain.value = plain => plain.text; 85 | 86 | /** 87 | * @function At 88 | * @param { number } target 89 | * @returns { MessageChain } 90 | */ 91 | const At = target => { 92 | return { 93 | type: 'At', 94 | target, 95 | }; 96 | }; 97 | /** 98 | * @method At#value 99 | * @param { MessageChain } at 100 | * @returns { at } 101 | */ 102 | At.value = at => { 103 | return { 104 | target: at.target, 105 | display: at.display, 106 | }; 107 | }; 108 | 109 | /** 110 | * @function AtAll 111 | * @returns { MessageChain } 112 | */ 113 | const AtAll = () => { 114 | return { 115 | type: 'AtAll', 116 | }; 117 | }; 118 | /** 119 | * @method AtAll#value 120 | * @returns { undefined } 121 | */ 122 | AtAll.value = () => {}; 123 | 124 | /** 125 | * @function Face 126 | * @param { number } faceId 127 | * @returns { MessageChain } 128 | */ 129 | const Face = faceId => { 130 | return { 131 | type: 'Face', 132 | faceId, 133 | }; 134 | }; 135 | /** 136 | * @method Face#value 137 | * @param { MessageChain } face 138 | * @returns { number } 139 | */ 140 | Face.value = face => face.faceId; 141 | /** 142 | * @function MarketFace 143 | * @param { string } name 144 | * @returns { MessageChain } 145 | */ 146 | const MarketFace = name => { 147 | return { 148 | type: 'MarketFace', 149 | name, 150 | }; 151 | }; 152 | /** 153 | * @function MarketFace#value 154 | * @param { MessageChain } face 155 | * @returns { string } 156 | */ 157 | MarketFace.value = face => face.name; 158 | 159 | /** 160 | * @function Image 161 | * @param { image } image 162 | * @returns { MessageChain } 163 | */ 164 | const Image = ({ imageId, url = '' }) => { 165 | return { 166 | type: 'Image', 167 | imageId, 168 | url, 169 | }; 170 | }; 171 | /** 172 | * @method Image#value 173 | * @param { MessageChain } image 174 | * @returns { image } 175 | */ 176 | Image.value = image => { 177 | return { 178 | imageId: image.imageId, 179 | url: image.url, 180 | }; 181 | }; 182 | 183 | /** 184 | * @function FlashImage 185 | * @param { image } image 186 | * @returns { MessageChain } 187 | */ 188 | const FlashImage = ({ imageId, url = '' }) => { 189 | return { 190 | type: 'FlashImage', 191 | imageId, 192 | url, 193 | }; 194 | }; 195 | /** 196 | * @method FlashImage#value 197 | * @param { MessageChain } image 198 | * @returns { image } 199 | */ 200 | FlashImage.value = image => { 201 | return { 202 | imageId: image.imageId, 203 | url: image.url, 204 | }; 205 | }; 206 | 207 | /** 208 | * @function Xml 209 | * @param { string } xml 210 | * @returns { MessageChain } 211 | */ 212 | const Xml = xml => { 213 | return { 214 | type: 'Xml', 215 | xml, 216 | }; 217 | }; 218 | /** 219 | * @method Xml#value 220 | * @param { MessageChain } xml 221 | * @returns { string } 222 | */ 223 | Xml.value = xml => xml.xml; 224 | 225 | /** 226 | * @function Json 227 | * @param { string } json 228 | * @returns { MessageChain } 229 | */ 230 | const Json = json => { 231 | return { 232 | type: 'Json', 233 | json, 234 | }; 235 | }; 236 | /** 237 | * @method Json#value 238 | * @param { MessageChain } json 239 | * @returns { string } 240 | */ 241 | Json.value = json => json.json; 242 | 243 | /** 244 | * @function App 245 | * @param { string } content 246 | * @returns { MessageChain } 247 | */ 248 | const App = content => { 249 | return { 250 | type: 'App', 251 | content, 252 | }; 253 | }; 254 | /** 255 | * @method App#value 256 | * @param { MessageChain } app 257 | * @returns { string } 258 | */ 259 | App.value = app => app.content; 260 | 261 | /** 262 | * @function Quote 263 | * @param { number } id 264 | * @returns { MessageChain } 265 | */ 266 | const Quote = id => { 267 | return { 268 | type: 'Quote', 269 | id, 270 | }; 271 | }; 272 | /** 273 | * @method Quote#value 274 | * @param { MessageChain } quote 275 | * @returns { quote } 276 | */ 277 | Quote.value = quote => { 278 | return { 279 | id: quote.id, 280 | groupId: quote.groupId, 281 | senderId: quote.senderId, 282 | origin: quote.origin, 283 | }; 284 | }; 285 | 286 | /** 287 | * @function Poke 288 | * @param { string } name 289 | * @returns { MessageChain } 290 | */ 291 | const Poke = name => { 292 | return { 293 | type: 'Poke', 294 | name, 295 | }; 296 | }; 297 | /** 298 | * @method Poke#value 299 | * @param { MessageChain } poke 300 | * @returns { string } 301 | */ 302 | Poke.value = poke => poke.name; 303 | 304 | /** 305 | * @function Voice 306 | * @param { voice } voice 307 | * @returns { MessageChain } 308 | */ 309 | const Voice = ({ voiceId, url = '' }) => { 310 | return { 311 | type: 'Voice', 312 | voiceId, 313 | url 314 | }; 315 | }; 316 | /** 317 | * @method Voice#value 318 | * @param { MessageChain } voice 319 | * @returns { voice } 320 | */ 321 | Voice.value = voice => { 322 | return { 323 | voiceId: voice.voiceId, 324 | url: voice.url 325 | } 326 | }; 327 | // Voice is deprecated since miria-core-2.7 328 | // TODO: Deprecate Voice and use Audio instead 329 | const Audio = Voice; 330 | 331 | /** 332 | * @function MusicShare 333 | * @param { music } music 334 | * @returns { MessageChain } 335 | */ 336 | const MusicShare = ({ 337 | kind, 338 | title, 339 | summary, 340 | jumpUrl, 341 | pictureUrl, 342 | musicUrl, 343 | brief, 344 | }) => { 345 | return { 346 | type: 'MusicShare', 347 | kind, title, summary, jumpUrl, pictureUrl, musicUrl, 348 | brief: brief || `[分享]${title}`, 349 | }; 350 | }; 351 | // seems this is unnecessary 352 | MusicShare.value = () => {}; 353 | 354 | /** 355 | * @function Dice 356 | * @param { number } value 骰子点数 357 | * @returns { MessageChain } 358 | */ 359 | const Dice = (value) => { 360 | return { 361 | type: 'Dice', 362 | value, 363 | }; 364 | }; 365 | /** 366 | * @method Dice#value 367 | * @param { MessageChain } dice 368 | * @returns { number } 369 | */ 370 | Dice.value = dice => { 371 | return dice.value; 372 | }; 373 | 374 | /** 375 | * @function Forward 转发消息 376 | * @param { nodeList | message[] | number[] } messages 可以传入消息数组、消息ID数组或自行构建`nodeList`,三者可以混合 377 | * @returns { MessageChain } 378 | */ 379 | const Forward = (messages) => { 380 | if (!Array.isArray(messages)) throw new Error('messages must be array') 381 | const nodeList = messages.map(/** @param {ForwardNode|message|number} msg */msg => { 382 | // 消息ID可以直接作为节点 383 | if (typeof msg === 'number') { 384 | return { messageId: msg }; 385 | } 386 | if (msg.sender) { 387 | /** @type { message } */ 388 | const time = msg.messageChain[0].type === 'Source' ? msg.messageChain[0].time : 0; 389 | return { 390 | senderId: msg.sender.id, 391 | time: 0, 392 | senderName: msg.sender.memberName || msg.sender.nickname, 393 | messageChain: msg.messageChain, 394 | messageId: null 395 | }; 396 | } 397 | /** @see https://github.com/project-mirai/mirai-api-http/issues/482 */ 398 | return Object.assign({ messageId: null }, msg); 399 | }) 400 | return { 401 | type: 'Forward', 402 | nodeList, 403 | }; 404 | }; 405 | /** 406 | * @function Forward#value 407 | * @param { MessageChain } forward 408 | * @returns { nodeList } 409 | */ 410 | Forward.value = forward => { 411 | return forward.nodeList; 412 | }; 413 | 414 | // TODO: Impl: File MiraiCode 415 | 416 | const encodeText = (text = '') => { 417 | return text 418 | .replace(/\\/g, '\\\\') // This must be first since others will add `\` 419 | .replace(/\[/g, '\\[') 420 | .replace(/]/g, '\\]') 421 | .replace(/:/g, '\\:') 422 | .replace(/,/g, '\\,') 423 | // .replace(/\n/g, '\\n') 424 | // .replace(/\r/g, '\\r') 425 | }; 426 | const decodeText = (text = '') => { 427 | // Better use reverted order as `encodeText` does 428 | return text 429 | // .replace(/\\r/g, '\r') 430 | // .replace(/\\n/g, '\n') 431 | .replace(/\\,/g, ',') 432 | .replace(/\\:/g, ':') 433 | .replace(/\\]/g, ']') 434 | .replace(/\\\[/g, '[') 435 | .replace(/\\\\/g, '\\') 436 | }; 437 | 438 | // Experimental 439 | // refer: https://github.com/mamoe/mirai/blob/dev/docs/Messages.md#%E6%B6%88%E6%81%AF%E9%93%BE%E7%9A%84-mirai-%E7%A0%81 440 | /** 441 | * @function toMiraiCode 442 | * @param { MessageChain } component 443 | * @returns { string } 444 | * @throws { TypeError } 445 | */ 446 | const toMiraiCode = component => { 447 | switch (component.type) { 448 | case 'Plain': 449 | return encodeText(component.text); 450 | case 'AtAll': 451 | return '[mirai:atall]'; 452 | case 'At': 453 | return `[mirai:at:${component.target}]`; 454 | case 'Face': 455 | return `[mirai:face:${component.faceId}]`; 456 | case 'MarketFace': 457 | return `[mirai:market:${encodeText(component.name)}]`; 458 | case 'Poke': 459 | // This is from mirai-api-http, not from mirai-core, so there is no `type` or `id` 460 | return `[mirai:poke:${encodeText(component.name)}]`; 461 | case 'VipFace': 462 | // TODO: 当前版本(http-api1.7.2)尚未支持 VipFace 消息 463 | // return `[mirai:vipface:${component.id},${component.name},${component.count}]`; 464 | break; 465 | case 'Image': 466 | // 可以加参数,但是旧版本不支持,并且发送图片不用自带这些参数 467 | // return `[mirai:image:${encodeText(component.imageId)},size=${component.size},type=${component.imageType},width=${component.width},height=${component.height},isEmoji=${component.isEmoji}]`; 468 | return `[mirai:image:${encodeText(component.imageId)}]`; 469 | case 'Voice': 470 | return `[mirai:voice:${component.voiceId}]`; 471 | case 'FlashImage': 472 | return `[mirai:flash:${encodeText(component.imageId)}]`; 473 | case 'Dice': 474 | return `[mirai:dice:${component.value}]`; 475 | case 'Quote': 476 | return `[mirai:quote:${component.id}]`; 477 | case 'Forward': 478 | return `[mirai:forward:${encodeText(JSON.stringify(component.nodeList))}]`; 479 | case 'App': 480 | return `[mirai:app:${encodeText(component.content)}]`; 481 | case 'Xml': 482 | return `[mirai:xml:${encodeText(component.xml)}]`; 483 | case 'File': 484 | return `[mirai:file:${encodeText(component.id)},${encodeText(component.name)},${component.size}]`; 485 | case 'MusicShare': 486 | const args = [ 487 | component.kind, 488 | component.title, 489 | component.summary, 490 | component.jumpUrl, 491 | component.pictureUrl, 492 | component.musicUrl, 493 | component.brief, 494 | ].map(str => encodeText(str)).join(',') 495 | return `[mirai:musicshare:${args}]`; 496 | } 497 | throw new TypeError(`Type ${component.type} is not yet supported`); 498 | }; 499 | 500 | /** 501 | * @function serialize 502 | * @description Stringify message components to mirai code string 503 | * @param { MessageChain[] } messageChain 504 | * @returns { string } 505 | */ 506 | const serialize = messageChain => { 507 | return messageChain.reduce((str, chain) => { 508 | if (chain.type === 'Source') return str; 509 | try { 510 | return str + toMiraiCode(chain); 511 | } catch (e) { 512 | // Maybe fall back to a common component `[mirai:type]`? 513 | // return str + `[mirai:${chain.type}]`; 514 | console.warn(`Unsupported message type ${chain.type} is skipped. This may cause some errors.`, e.message || e); 515 | return str; 516 | } 517 | }, ''); 518 | }; 519 | 520 | // TODO: 521 | /** 522 | * @function deserialize 523 | * @description Parse serialized mirai code string to message components 524 | * @param { string } string 525 | * @returns { MessageChain[] } 526 | */ 527 | const deserialize = string => { 528 | // Split from `[` or `]`, but not `\\[` or `\\]` 529 | // Even items are plain texts, and odd items are components starts like `mirai:type[:args]` 530 | const codes = string.split(/(? { 534 | if (index % 2) { 535 | // Is message component 536 | if (!c.startsWith('mirai:')) throw new Error(`Invalid serialized mirai code [${c}]`); 537 | const [_, type, ...args] = c.split(':'); 538 | const arguments = args.join(':').split(/(? i !== null); 580 | }; 581 | 582 | module.exports = { 583 | Source, 584 | Plain, 585 | At, 586 | AtAll, 587 | Face, 588 | Image, 589 | FlashImage, 590 | Xml, 591 | Json, 592 | App, 593 | Quote, 594 | Poke, 595 | Voice, 596 | Audio, 597 | MusicShare, 598 | Dice, 599 | Forward, 600 | toMiraiCode, 601 | serialize, 602 | deserialize, 603 | }; 604 | -------------------------------------------------------------------------------- /event.md: -------------------------------------------------------------------------------- 1 | # 事件订阅说明 2 | 3 | ## 接口 4 | 5 | ```javascript 6 | bot.on(eventName, callback); 7 | ``` 8 | 9 | ### 例 10 | 11 | ```javascript 12 | bot.on('mute', ({ operator }) => console.log(`我被${operator.memberName}禁言啦!`)); 13 | ``` 14 | 15 | ## 事件名称一览表 16 | 17 | **Tips**: node-mirai 的 **事件名称 (eventName)** 和 mirai-api-http 的 **事件类型 (type)** 并不一致。 18 | 19 | 通常情况下,node-mirai 中的事件名称是 mirai-api-http 中的事件类型去掉 `Bot` 和 `Event` 后的小驼峰命名形式。在监听事件时,请以 node-mirai 的事件名称为准。例如: 20 | 21 | ```javascript 22 | /* 监听群成员主动离群事件 (MemberLeaveEventQuit => memberLeaveQuit) */ 23 | 24 | // 正确写法:监听 memberLeaveQuit 25 | bot.on("memberLeaveQuit", () => console.log("Someone has left the group.")); 26 | 27 | // 错误写法:监听 MemberLeaveEventQuit 28 | bot.on("MemberLeaveEventQuit", () => console.log("Someone has left the group.")); 29 | ``` 30 | 31 | 下表列出了 mirai-api-http 事件类型与 node-mirai 事件名称的对应关系: 32 | 33 | | mirai-api-http 事件类型 | 对应 node-mirai 事件名称 | 34 | |-----------------------|------------------------| 35 | |BotOnlineEvent|online| 36 | |BotOfflineEventActive|offlineActive| 37 | |BotOfflineEventForce|offlineForce| 38 | |BotOfflineEventDropped|offlineDropped| 39 | |BotReloginEvent|relogin| 40 | |BotGroupPermissionChangeEvent|groupPermissionChange| 41 | |BotMuteEvent|mute| 42 | |BotUnmuteEvent|unmute| 43 | |BotLeaveEventActive|leaveActive| 44 | |BotLeaveEventKick|leaveKick| 45 | |BotJoinGroupEvent|joinGroup| 46 | |BotInvitedJoinGroupRequestEvent|invitedJoinGroupRequest| 47 | |GroupNameChangeEvent|groupNameChange| 48 | |GroupEntranceAnnouncementChangeEvent|groupEntranceAnnouncementChange| 49 | |GroupMuteAllEvent|groupMuteAll| 50 | |GroupAllowAnonymousChatEvent|groupAllowAnonymousChat| 51 | |GroupAllowConfessTalkEvent|groupAllowConfessTalk| 52 | |GroupAllowMemberInviteEvent|groupAllowMemberInvite| 53 | |GroupRecallEvent|groupRecall| 54 | |FriendRecallEvent|friendRecall| 55 | |MemberJoinEvent|memberJoin| 56 | |MemberLeaveEventKick|memberLeaveKick| 57 | |MemberLeaveEventQuit|memberLeaveQuit| 58 | |MemberCardChangeEvent|memberCardChange| 59 | |MemberSpecialTitleChangeEvent|memberSpecialTitleChange| 60 | |MemberPermissionChangeEvent|memberPermissionChange| 61 | |MemberMuteEvent|memberMute| 62 | |MemberUnmuteEvent|memberUnmute| 63 | |MemberJoinRequestEvent|memberJoinRequest| 64 | |NewFriendRequestEvent|newFriendRequest| 65 | 66 | 另请参阅 [events.json](src/events.json). 67 | 68 | ## 所有事件及 callback 参数 69 | 70 | 此列表来自 [mirai-http-api/docs/EventType](https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md) 71 | 72 | ### Bot登录成功 73 | 74 | eventName: `online` 75 | 76 | callback parameter: 77 | 78 | ```json5 79 | { 80 | "type": "BotOnlineEvent", 81 | "qq": 123456 82 | } 83 | ``` 84 | 85 | | 名字 | 类型 | 说明 | 86 | | ---- | ---- | ------------------- | 87 | | qq | Long | 登录成功的Bot的QQ号 | 88 | 89 | ### Bot主动离线 90 | 91 | eventName: `offlineActive` 92 | 93 | callback parameter: 94 | 95 | ```json5 96 | { 97 | "type": "BotOfflineEventActive", 98 | "qq": 123456 99 | } 100 | ``` 101 | 102 | | 名字 | 类型 | 说明 | 103 | | ---- | ---- | ------------------- | 104 | | qq | Long | 主动离线的Bot的QQ号 | 105 | 106 | ### Bot被挤下线 107 | 108 | eventName: `offlineForce` 109 | 110 | callback parameter: 111 | 112 | ```json5 113 | { 114 | "type": "BotOfflineEventForce", 115 | "qq": 123456 116 | } 117 | ``` 118 | 119 | | 名字 | 类型 | 说明 | 120 | | ---- | ---- | ------------------- | 121 | | qq | Long | 被挤下线的Bot的QQ号 | 122 | 123 | ### Bot被服务器断开或因网络问题而掉线 124 | 125 | eventName: `offlineDropped` 126 | 127 | callback parameter: 128 | 129 | ```json5 130 | { 131 | "type": "BotOfflineEventDropped", 132 | "qq": 123456 133 | } 134 | ``` 135 | 136 | | 名字 | 类型 | 说明 | 137 | | ---- | ---- | ----------------------------------------- | 138 | | qq | Long | 被服务器断开或因网络问题而掉线的Bot的QQ号 | 139 | 140 | ### Bot主动重新登录 141 | 142 | eventName: `relogin` 143 | 144 | callback parameter: 145 | 146 | ```json5 147 | { 148 | "type": "BotReloginEvent", 149 | "qq": 123456 150 | } 151 | ``` 152 | 153 | | 名字 | 类型 | 说明 | 154 | | ---- | ---- | ----------------------- | 155 | | qq | Long | 主动重新登录的Bot的QQ号 | 156 | 157 | ### Bot在群里的权限被改变. 操作人一定是群主 158 | 159 | eventName: `groupPermissionChange` 160 | 161 | callback parameter: 162 | 163 | ```json5 164 | { 165 | "type": "BotGroupPermissionChangeEvent", 166 | "origin": "MEMBER", 167 | "new": "ADMINISTRATOR", 168 | "group": { 169 | "id": 123456789, 170 | "name": "Miral Technology", 171 | "permission": "ADMINISTRATOR" 172 | } 173 | } 174 | ``` 175 | 176 | | 名字 | 类型 | 说明 | 177 | | ---------------- | ------ | --------------------------------------------- | 178 | | origin | String | Bot的原权限,OWNER、ADMINISTRATOR或MEMBER | 179 | | new | String | Bot的新权限,OWNER、ADMINISTRATOR或MEMBER | 180 | | group | Object | 权限改变所在的群信息 | 181 | | group.id | Long | 群号 | 182 | | group.name | String | 群名 | 183 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 184 | 185 | ### Bot被禁言 186 | 187 | eventName: `mute` 188 | 189 | callback parameter: 190 | 191 | ```json5 192 | { 193 | "type": "BotMuteEvent", 194 | "durationSeconds": 600, 195 | "operator": { 196 | "id": 123456789, 197 | "memberName": "我是管理员", 198 | "permission": "ADMINISTRATOR", 199 | "group": { 200 | "id": 123456789, 201 | "name": "Miral Technology", 202 | "permission": "MEMBER" 203 | } 204 | } 205 | } 206 | ``` 207 | 208 | | 名字 | 类型 | 说明 | 209 | | ------------------------- | ------ | ------------------------------------------------ | 210 | | durationSeconds | Int | 禁言时长,单位为秒 | 211 | | operator | Object | 操作的管理员或群主信息 | 212 | | operator.id | Long | 操作者的QQ号 | 213 | | operator.memberName | String | 操作者的群名片 | 214 | | operator.permission | String | 操作者在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 215 | | operator.group | Object | Bot被禁言所在群的信息 | 216 | | operator.group.id | Long | 群号 | 217 | | operator.group.name | String | 群名 | 218 | | operator.group.permission | String | Bot在群中的权限,OWNER或ADMINISTRATOR | 219 | 220 | ### Bot被取消禁言 221 | 222 | eventName: `unmute` 223 | 224 | callback parameter: 225 | 226 | ```json5 227 | { 228 | "type": "BotUnmuteEvent", 229 | "operator": { 230 | "id": 123456789, 231 | "memberName": "我是管理员", 232 | "permission": "ADMINISTRATOR", 233 | "group": { 234 | "id": 123456789, 235 | "name": "Miral Technology", 236 | "permission": "MEMBER" 237 | } 238 | } 239 | } 240 | ``` 241 | 242 | | 名字 | 类型 | 说明 | 243 | | ------------------------- | ------ | ------------------------------------------------ | 244 | | operator | Object | 操作的管理员或群主信息 | 245 | | operator.id | Long | 操作者的QQ号 | 246 | | operator.memberName | String | 操作者的群名片 | 247 | | operator.permission | String | 操作者在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 248 | | operator.group | Object | Bot被取消禁言所在群的信息 | 249 | | operator.group.id | Long | 群号 | 250 | | operator.group.name | String | 群名 | 251 | | operator.group.permission | String | Bot在群中的权限,OWNER或ADMINISTRATOR | 252 | 253 | ### Bot加入了一个新群 254 | 255 | eventName: `joinGroup` 256 | 257 | callback parameter: 258 | 259 | ```json5 260 | { 261 | "type": "BotJoinGroupEvent", 262 | "group": { 263 | "id": 123456789, 264 | "name": "Miral Technology", 265 | "permission": "MEMBER" 266 | } 267 | } 268 | ``` 269 | 270 | | 名字 | 类型 | 说明 | 271 | | ---------------- | ------ | ------------------------------------------------------------ | 272 | | group | Object | Bot新加入群的信息 | 273 | | group.id | Long | 群号 | 274 | | group.name | String | 群名 | 275 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER(新加入群通常是Member) | 276 | 277 | ### 某个群名改变 278 | 279 | eventName: `groupNameChange` 280 | 281 | callback parameter: 282 | 283 | ```json5 284 | { 285 | "type": "GroupNameChangeEvent", 286 | "origin": "miral technology", 287 | "new": "MIRAI TECHNOLOGY", 288 | "group": { 289 | "id": 123456789, 290 | "name": "MIRAI TECHNOLOGY", 291 | "permission": "MEMBER" 292 | }, 293 | "isByBot": false 294 | } 295 | ``` 296 | 297 | | 名字 | 类型 | 说明 | 298 | | ---------------- | ------- | --------------------------------------------- | 299 | | origin | String | 原群名 | 300 | | new | String | 新群名 | 301 | | group | Object | 群名改名的群信息 | 302 | | group.id | Long | 群号 | 303 | | group.name | String | 群名 | 304 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 305 | | isByBot | Boolean | 是否Bot进行该操作 | 306 | 307 | ### 某群入群公告改变 308 | 309 | eventName: `groupEntranceAnnouncementChange` 310 | 311 | callback parameter: 312 | 313 | ```json5 314 | { 315 | "type": "GroupEntranceAnnouncementChangeEvent", 316 | "origin": "abc", 317 | "new": "cba", 318 | "group": { 319 | "id": 123456789, 320 | "name": "Miral Technology", 321 | "permission": "MEMBER" 322 | }, 323 | "operator": { 324 | "id": 123456789, 325 | "memberName": "我是管理员", 326 | "permission": "ADMINISTRATOR", 327 | "group": { 328 | "id": 123456789, 329 | "name": "Miral Technology", 330 | "permission": "MEMBER" 331 | } 332 | } 333 | } 334 | ``` 335 | 336 | | 名字 | 类型 | 说明 | 337 | | ------------------- | ------- | --------------------------------------------- | 338 | | origin | String | 原公告 | 339 | | new | String | 新公告 | 340 | | group | Object | 公告改变的群信息 | 341 | | group.id | Long | 群号 | 342 | | group.name | String | 群名 | 343 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 344 | | operator | Object? | 操作的管理员或群主信息,当null时为Bot操作 | 345 | | operator.id | Long | 操作者的QQ号 | 346 | | operator.memberName | String | 操作者的群名片 | 347 | | operator.permission | String | 操作者在群中的权限,OWNER或ADMINISTRATOR | 348 | | operator.group | Object | 同group | 349 | 350 | ### 全员禁言 351 | 352 | eventName: `groupMuteAll` 353 | 354 | callback parameter: 355 | 356 | ```json5 357 | { 358 | "type": "GroupMuteAllEvent", 359 | "origin": false, 360 | "new": true, 361 | "group": { 362 | "id": 123456789, 363 | "name": "Miral Technology", 364 | "permission": "MEMBER" 365 | }, 366 | "operator": { 367 | "id": 123456789, 368 | "memberName": "我是管理员", 369 | "permission": "ADMINISTRATOR", 370 | "group": { 371 | "id": 123456789, 372 | "name": "Miral Technology", 373 | "permission": "MEMBER" 374 | } 375 | } 376 | } 377 | ``` 378 | 379 | | 名字 | 类型 | 说明 | 380 | | ------------------- | ------- | --------------------------------------------- | 381 | | origin | Boolean | 原本是否处于全员禁言 | 382 | | new | Boolean | 现在是否处于全员禁言 | 383 | | group | Object | 全员禁言的群信息 | 384 | | group.id | Long | 群号 | 385 | | group.name | String | 群名 | 386 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 387 | | operator | Object? | 操作的管理员或群主信息,当null时为Bot操作 | 388 | | operator.id | Long | 操作者的QQ号 | 389 | | operator.memberName | String | 操作者的群名片 | 390 | | operator.permission | String | 操作者在群中的权限,OWNER或ADMINISTRATOR | 391 | | operator.group | Object | 同group | 392 | 393 | ### 匿名聊天 394 | 395 | eventName: `groupAllowAnonymousChat` 396 | 397 | callback parameter: 398 | 399 | ```json5 400 | { 401 | "type": "GroupAllowAnonymousChatEvent", 402 | "origin": false, 403 | "new": true, 404 | "group": { 405 | "id": 123456789, 406 | "name": "Miral Technology", 407 | "permission": "MEMBER" 408 | }, 409 | "operator": { 410 | "id": 123456789, 411 | "memberName": "我是管理员", 412 | "permission": "ADMINISTRATOR", 413 | "group": { 414 | "id": 123456789, 415 | "name": "Miral Technology", 416 | "permission": "MEMBER" 417 | } 418 | } 419 | } 420 | ``` 421 | 422 | | 名字 | 类型 | 说明 | 423 | | ------------------- | ------- | --------------------------------------------- | 424 | | origin | Boolean | 原本匿名聊天是否开启 | 425 | | new | Boolean | 现在匿名聊天是否开启 | 426 | | group | Object | 匿名聊天状态改变的群信息 | 427 | | group.id | Long | 群号 | 428 | | group.name | String | 群名 | 429 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 430 | | operator | Object? | 操作的管理员或群主信息,当null时为Bot操作 | 431 | | operator.id | Long | 操作者的QQ号 | 432 | | operator.memberName | String | 操作者的群名片 | 433 | | operator.permission | String | 操作者在群中的权限,OWNER或ADMINISTRATOR | 434 | | operator.group | Object | 同group | 435 | 436 | ### 坦白说 437 | 438 | eventName: `groupAllowConfessTalk` 439 | 440 | callback parameter: 441 | 442 | ```json5 443 | { 444 | "type": "GroupAllowConfessTalkEvent", 445 | "origin": false, 446 | "new": true, 447 | "group": { 448 | "id": 123456789, 449 | "name": "Miral Technology", 450 | "permission": "MEMBER" 451 | }, 452 | "isByBot": false 453 | } 454 | ``` 455 | 456 | | 名字 | 类型 | 说明 | 457 | | ---------------- | ------- | --------------------------------------------- | 458 | | origin | Boolean | 原本坦白说是否开启 | 459 | | new | Boolean | 现在坦白说是否开启 | 460 | | group | Object | 坦白说状态改变的群信息 | 461 | | group.id | Long | 群号 | 462 | | group.name | String | 群名 | 463 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 464 | | isByBot | Boolean | 是否Bot进行该操作 | 465 | 466 | ### 允许群员邀请好友加群 467 | 468 | eventName: `groupAllowMemberInvite` 469 | 470 | callback parameter: 471 | 472 | ```json5 473 | { 474 | "type": "GroupAllowMemberInviteEvent", 475 | "origin": false, 476 | "new": true, 477 | "group": { 478 | "id": 123456789, 479 | "name": "Miral Technology", 480 | "permission": "MEMBER" 481 | }, 482 | "operator": { 483 | "id": 123456789, 484 | "memberName": "我是管理员", 485 | "permission": "ADMINISTRATOR", 486 | "group": { 487 | "id": 123456789, 488 | "name": "Miral Technology", 489 | "permission": "MEMBER" 490 | } 491 | } 492 | } 493 | ``` 494 | 495 | | 名字 | 类型 | 说明 | 496 | | ------------------- | ------- | --------------------------------------------- | 497 | | origin | Boolean | 原本是否允许群员邀请好友加群 | 498 | | new | Boolean | 现在是否允许群员邀请好友加群 | 499 | | group | Object | 允许群员邀请好友加群状态改变的群信息 | 500 | | group.id | Long | 群号 | 501 | | group.name | String | 群名 | 502 | | group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 503 | | operator | Object? | 操作的管理员或群主信息,当null时为Bot操作 | 504 | | operator.id | Long | 操作者的QQ号 | 505 | | operator.memberName | String | 操作者的群名片 | 506 | | operator.permission | String | 操作者在群中的权限,OWNER或ADMINISTRATOR | 507 | | operator.group | Object | 同group | 508 | 509 | ### 新人入群的事件 510 | 511 | eventName: `memberJoin` 512 | 513 | callback parameter: 514 | 515 | ```json5 516 | { 517 | "type": "MemberJoinEvent", 518 | "member": { 519 | "id": 123456789, 520 | "memberName": "我是新人", 521 | "permission": "MEMBER", 522 | "group": { 523 | "id": 123456789, 524 | "name": "Miral Technology", 525 | "permission": "MEMBER" 526 | } 527 | } 528 | } 529 | ``` 530 | 531 | | 名字 | 类型 | 说明 | 532 | | ----------------------- | ------ | ------------------------------------------------------------ | 533 | | member | Object | 新人信息 | 534 | | member.id | Long | 新人的QQ号 | 535 | | member.memberName | String | 新人的群名片 | 536 | | member.permission | String | 新人在群中的权限,OWNER、ADMINISTRATOR或MEMBER(新入群通常是MEMBER) | 537 | | member.group | Object | 新人入群的群信息 | 538 | | member.group.id | Long | 群号 | 539 | | member.group.name | String | 群名 | 540 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 541 | 542 | ### 成员被踢出群(该成员不是Bot) 543 | 544 | eventName: `memberLeaveKick` 545 | 546 | callback parameter: 547 | 548 | ```json5 549 | { 550 | "type": "MemberLeaveEventKick", 551 | "member": { 552 | "id": 123456789, 553 | "memberName": "我是被踢的", 554 | "permission": "MEMBER", 555 | "group": { 556 | "id": 123456789, 557 | "name": "Miral Technology", 558 | "permission": "MEMBER" 559 | } 560 | }, 561 | "operator": { 562 | "id": 123456789, 563 | "memberName": "我是管理员", 564 | "permission": "ADMINISTRATOR", 565 | "group": { 566 | "id": 123456789, 567 | "name": "Miral Technology", 568 | "permission": "MEMBER" 569 | } 570 | } 571 | } 572 | ``` 573 | 574 | | 名字 | 类型 | 说明 | 575 | | ----------------------- | ------- | --------------------------------------------- | 576 | | member | Object | 被踢者的信息 | 577 | | member.id | Long | 被踢者的QQ号 | 578 | | member.memberName | String | 被踢者的群名片 | 579 | | member.permission | String | 被踢者在群中的权限,ADMINISTRATOR或MEMBER | 580 | | member.group | Object | 被踢者所在的群 | 581 | | member.group.id | Long | 群号 | 582 | | member.group.name | String | 群名 | 583 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 584 | | operator | Object? | 操作的管理员或群主信息,当null时为Bot操作 | 585 | | operator.id | Long | 操作者的QQ号 | 586 | | operator.memberName | String | 操作者的群名片 | 587 | | operator.permission | String | 操作者在群中的权限,OWNER或ADMINISTRATOR | 588 | | operator.group | Object | 同member.group | 589 | 590 | ### 成员主动离群(该成员不是Bot) 591 | 592 | eventName: `memberLeaveQuit` 593 | 594 | callback parameter: 595 | 596 | ```json5 597 | { 598 | "type": "MemberLeaveEventQuit", 599 | "member": { 600 | "id": 123456789, 601 | "memberName": "我是被踢的", 602 | "permission": "MEMBER", 603 | "group": { 604 | "id": 123456789, 605 | "name": "Miral Technology", 606 | "permission": "MEMBER" 607 | } 608 | } 609 | } 610 | ``` 611 | 612 | | 名字 | 类型 | 说明 | 613 | | ----------------------- | ------ | --------------------------------------------- | 614 | | member | Object | 退群群员的信息 | 615 | | member.id | Long | 退群群员的QQ号 | 616 | | member.memberName | String | 退群群员的群名片 | 617 | | member.permission | String | 退群群员在群中的权限,ADMINISTRATOR或MEMBER | 618 | | member.group | Object | 退群群员所在的群信息 | 619 | | member.group.id | Long | 群号 | 620 | | member.group.name | String | 群名 | 621 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 622 | 623 | ### 群名片改动 624 | 625 | eventName: `memberCardChange` 626 | 627 | callback parameter: 628 | 629 | ```json5 630 | { 631 | "type": "MemberCardChangeEvent", 632 | "origin": "origin name", 633 | "new": "我是被改名的", 634 | "member": { 635 | "id": 123456789, 636 | "memberName": "我是被改名的", 637 | "permission": "MEMBER", 638 | "group": { 639 | "id": 123456789, 640 | "name": "Miral Technology", 641 | "permission": "MEMBER" 642 | } 643 | }, 644 | "operator": { 645 | "id": 123456789, 646 | "memberName": "我是管理员,也可能是我自己", 647 | "permission": "ADMINISTRATOR", 648 | "group": { 649 | "id": 123456789, 650 | "name": "Miral Technology", 651 | "permission": "MEMBER" 652 | } 653 | } 654 | } 655 | ``` 656 | 657 | | 名字 | 类型 | 说明 | 658 | | ----------------------- | ------- | -------------------------------------------------------- | 659 | | member | Object | 名片改动的群员的信息 | 660 | | member.id | Long | 名片改动的群员的QQ号 | 661 | | member.memberName | String | 名片改动的群员的群名片 | 662 | | member.permission | String | 名片改动的群员在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 663 | | member.group | Object | 名片改动的群员所在群的信息 | 664 | | member.group.id | Long | 群号 | 665 | | member.group.name | String | 群名 | 666 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 667 | | operator | Object? | 操作者的信息,可能为该群员自己,当null时为Bot操作 | 668 | | operator.id | Long | 操作者的QQ号 | 669 | | operator.memberName | String | 操作者的群名片 | 670 | | operator.permission | String | 操作者在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 671 | | operator.group | Object | 同member.group | 672 | 673 | ### 群头衔改动(只有群主有操作限权) 674 | 675 | eventName: `memberSpecialTitleChange` 676 | 677 | callback parameter: 678 | 679 | ```json5 680 | { 681 | "type": "MemberSpecialTitleChangeEvent", 682 | "origin": "origin title", 683 | "new": "new title", 684 | "member": { 685 | "id": 123456789, 686 | "memberName": "我是被改头衔的", 687 | "permission": "MEMBER", 688 | "group": { 689 | "id": 123456789, 690 | "name": "Miral Technology", 691 | "permission": "MEMBER" 692 | } 693 | } 694 | } 695 | ``` 696 | 697 | | 名字 | 类型 | 说明 | 698 | | ----------------------- | ------ | -------------------------------------------------------- | 699 | | origin | String | 原头衔 | 700 | | new | String | 现头衔 | 701 | | member | Object | 头衔改动的群员的信息 | 702 | | member.id | Long | 头衔改动的群员的QQ号 | 703 | | member.memberName | String | 头衔改动的群员的群名片 | 704 | | member.permission | String | 头衔改动的群员在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 705 | | member.group | Object | 头衔改动的群员所在群的信息 | 706 | | member.group.id | Long | 群号 | 707 | | member.group.name | String | 群名 | 708 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 709 | 710 | ### 成员权限改变的事件(该成员不可能是Bot,见BotGroupPermissionChangeEvent) 711 | 712 | eventName: `memberPermissionChange` 713 | 714 | callback parameter: 715 | 716 | ```json5 717 | { 718 | "type": "MemberPermissionChangeEvent", 719 | "origin": "MEMBER", 720 | "new": "ADMINISTRATOR", 721 | "member": { 722 | "id": 123456789, 723 | "memberName": "我是被改权限的", 724 | "permission": "ADMINISTRATOR", 725 | "group": { 726 | "id": 123456789, 727 | "name": "Miral Technology", 728 | "permission": "MEMBER" 729 | } 730 | } 731 | } 732 | ``` 733 | 734 | | 名字 | 类型 | 说明 | 735 | | ----------------------- | ------ | ------------------------------------------------- | 736 | | origin | String | 原权限 | 737 | | new | String | 现权限 | 738 | | member | Object | 权限改动的群员的信息 | 739 | | member.id | Long | 权限改动的群员的QQ号 | 740 | | member.memberName | String | 权限改动的群员的群名片 | 741 | | member.permission | String | 权限改动的群员在群中的权限,ADMINISTRATOR或MEMBER | 742 | | member.group | Object | 权限改动的群员所在群的信息 | 743 | | member.group.id | Long | 群号 | 744 | | member.group.name | String | 群名 | 745 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 746 | 747 | ### 群成员被禁言事件(该成员不可能是Bot,见BotMuteEvent) 748 | 749 | eventName: `memberMute` 750 | 751 | callback parameter: 752 | 753 | ```json5 754 | { 755 | "type": "MemberMuteEvent", 756 | "durationSeconds": 600, 757 | "member": { 758 | "id": 123456789, 759 | "memberName": "我是被禁言的", 760 | "permission": "MEMBER", 761 | "group": { 762 | "id": 123456789, 763 | "name": "Miral Technology", 764 | "permission": "MEMBER" 765 | } 766 | }, 767 | "operator": { 768 | "id": 123456789, 769 | "memberName": "我是管理员", 770 | "permission": "ADMINISTRATOR", 771 | "group": { 772 | "id": 123456789, 773 | "name": "Miral Technology", 774 | "permission": "MEMBER" 775 | } 776 | } 777 | } 778 | ``` 779 | 780 | | 名字 | 类型 | 说明 | 781 | | ----------------------- | ------- | ----------------------------------------------- | 782 | | durationSeconds | Long | 禁言时长,单位为秒 | 783 | | member | Object | 被禁言的群员的信息 | 784 | | member.id | Long | 被禁言的群员的QQ号 | 785 | | member.memberName | String | 被禁言的群员的群名片 | 786 | | member.permission | String | 被禁言的群员在群中的权限,ADMINISTRATOR或MEMBER | 787 | | member.group | Object | 被禁言的群员所在群的信息 | 788 | | member.group.id | Long | 群号 | 789 | | member.group.name | String | 群名 | 790 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 791 | | operator | Object? | 操作者的信息,当null时为Bot操作 | 792 | | operator.id | Long | 操作者的QQ号 | 793 | | operator.memberName | String | 操作者的群名片 | 794 | | operator.permission | String | 操作者在群中的权限,OWNER、ADMINISTRATOR | 795 | | operator.group | Object | 同member.group | 796 | 797 | ### 群成员被取消禁言事件(该成员不可能是Bot,见BotUnmuteEvent) 798 | 799 | eventName: `memberUnmute` 800 | 801 | callback parameter: 802 | 803 | ```json5 804 | { 805 | "type": "MemberUnmuteEvent", 806 | "member": { 807 | "id": 123456789, 808 | "memberName": "我是被取消禁言的", 809 | "permission": "MEMBER", 810 | "group": { 811 | "id": 123456789, 812 | "name": "Miral Technology", 813 | "permission": "MEMBER" 814 | } 815 | }, 816 | "operator": { 817 | "id": 123456789, 818 | "memberName": "我是管理员", 819 | "permission": "ADMINISTRATOR", 820 | "group": { 821 | "id": 123456789, 822 | "name": "Miral Technology", 823 | "permission": "MEMBER" 824 | } 825 | } 826 | } 827 | ``` 828 | 829 | | 名字 | 类型 | 说明 | 830 | | ----------------------- | ------- | --------------------------------------------------- | 831 | | member | Object | 被取消禁言的群员的信息 | 832 | | member.id | Long | 被取消禁言的群员的QQ号 | 833 | | member.memberName | String | 被取消禁言的群员的群名片 | 834 | | member.permission | String | 被取消禁言的群员在群中的权限,ADMINISTRATOR或MEMBER | 835 | | member.group | Object | 被取消禁言的群员所在群的信息 | 836 | | member.group.id | Long | 群号 | 837 | | member.group.name | String | 群名 | 838 | | member.group.permission | String | Bot在群中的权限,OWNER、ADMINISTRATOR或MEMBER | 839 | | operator | Object? | 操作者的信息,当null时为Bot操作 | 840 | | operator.id | Long | 操作者的QQ号 | 841 | | operator.memberName | String | 操作者的群名片 | 842 | | operator.permission | String | 操作者在群中的权限,OWNER、ADMINISTRATOR | 843 | | operator.group | Object | 同member.group | 844 | 845 | ### 用户入群申请(Bot需要有管理员权限) 846 | 847 | eventName: `memberJoinRequest` 848 | 849 | callback parameter: 850 | 851 | ```json5 852 | { 853 | "type": "MemberJoinRequestEvent", 854 | "eventId": 12345678, 855 | "fromId": 123456, 856 | "groupId": 654321, 857 | "groupName": "Group", 858 | "nick": "Nick Name" 859 | } 860 | ``` 861 | 862 | | 名字 | 类型 | 说明 | 863 | | ---------- | ------ | ----------------------- | 864 | | eventId | Long | 事件标识,响应该事件时的标识 | 865 | | fromId | Long | 申请人QQ号 | 866 | | groupId | Long | 申请人申请入群的群号 | 867 | | groupName | String | 申请人申请入群的群名称 | 868 | | nick | String | 申请人的昵称或群名片 | 869 | 870 | 接收 `memberJoinRequest` 事件后,可以通过 `bot.handleMemberJoinRequest(eventId, fromId, groupId, operate, message)` 处理用户入群申请。 871 | 872 | ### Bot被邀请入群申请 873 | 874 | eventName: `invitedJoinGroupRequest` 875 | 876 | callback parameter: 877 | 878 | ```json 879 | { 880 | "type": "BotInvitedJoinGroupRequestEvent", 881 | "eventId": 12345678, 882 | "fromId": 123456, 883 | "groupId": 654321, 884 | "groupName": "Group", 885 | "nick": "Nick Name", 886 | "message": "" 887 | } 888 | ``` 889 | 890 | | 名字 | 类型 | 说明 | 891 | |-----------|----------|--------------------------| 892 | | eventId | Long | 事件标识,响应该事件时的标识 | 893 | | fromId | Long | 邀请人(好友)的QQ号 | 894 | | groupId | Long | 被邀请进入群的群号 | 895 | | groupName | String | 被邀请进入群的群名称 | 896 | | nick | String | 邀请人(好友)的昵称 | 897 | | message | String | 邀请消息 | 898 | 899 | 接收 `invitedJoinGroupRequest` 事件后,可以通过 `bot.handleBotInvitedJoinGroupRequest(eventId, fromId, groupId, operate, message)` 处理入群邀请。 900 | 901 | ### Bot主动退出一个群 902 | 903 | eventName: `leaveActive` 904 | 905 | callback parameter: 906 | 907 | ```json5 908 | { 909 | "type": "BotLeaveEventActive", 910 | "group": { 911 | "id": 123456789, 912 | "name": "Miral Technology", 913 | "permission": "MEMBER" 914 | } 915 | } 916 | ``` 917 | 918 | | 名字 | 类型 | 说明 | 919 | | ---------------- | ------ | ------------------------------------- | 920 | | group | Object | Bot退出的群的信息 | 921 | | group.id | Long | 群号 | 922 | | group.name | String | 群名 | 923 | | group.permission | String | Bot在群中的权限,ADMINISTRATOR或MEMBER | 924 | 925 | ### Bot被踢出一个群 926 | 927 | eventName: `leaveKick` 928 | 929 | callback parameter: 930 | 931 | ```json5 932 | { 933 | "type": "BotLeaveEventKick", 934 | "group": { 935 | "id": 123456789, 936 | "name": "Miral Technology", 937 | "permission": "MEMBER" 938 | } 939 | } 940 | ``` 941 | 942 | | 名字 | 类型 | 说明 | 943 | | ---------------- | ------ | ------------------------------------------------------------ | 944 | | group | Object | Bot被踢出的群的信息 | 945 | | group.id | Long | 群号 | 946 | | group.name | String | 群名 | 947 | | group.permission | String | Bot在群中的权限,ADMINISTRATOR或MEMBER | 948 | 949 | ### Bot接收到添加好友申请 950 | 951 | eventName: `newFriendRequest` 952 | 953 | callback parameter: 954 | 955 | ```json5 956 | { 957 | "type": "NewFriendRequestEvent", 958 | "eventId": 12345678, 959 | "fromId": 123456, 960 | "groupId": 654321, 961 | "nick": "Nick Name", 962 | "message": "" 963 | } 964 | ``` 965 | 966 | | 名字 | 类型 | 说明 | 967 | | ------- | ------ | ----------------------------------------------- | 968 | | eventId | Long | 事件标识,响应该事件时的标识 | 969 | | fromId | Long | 申请人QQ号 | 970 | | groupId | Long | 申请人如果通过某个群添加好友,该项为该群群号;否则为0 | 971 | | nick | String | 申请人的昵称或群名片 | 972 | | message | String | 申请消息 | 973 | 974 | 接收 `newFriendRequest` 事件后,可以通过 `bot.handleNewFriendRequest(eventId, fromId, groupId, operate, message)` 处理添加好友申请。 975 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | /** 3 | * @type { import('./src/typedef').WebSocket } 4 | */ 5 | const WebSocket = require('ws'); 6 | const semver = require('semver'); 7 | const { default: axios } = require('axios'); 8 | 9 | const Signal = require('./src/utils/Signal'); 10 | const checkMAHVersion = require('./src/utils/checkMAHVersion'); 11 | 12 | const MessageComponent = require('./src/MessageComponent'); 13 | const Target = require('./src/target'); 14 | const events = require('./src/events.json'); 15 | 16 | const { Plain } = MessageComponent; 17 | 18 | const init = require('./src/init'); 19 | const verify = require('./src/verify'); 20 | const release = require('./src/release'); 21 | const fetchMessage = require('./src/fetchMessage'); 22 | const recall = require('./src/recall'); 23 | 24 | const { 25 | sendFriendMessage, 26 | sendGroupMessage, 27 | sendTempMessage, 28 | sendQuotedFriendMessage, 29 | sendQuotedGroupMessage, 30 | sendQuotedTempMessage, 31 | uploadImage, 32 | uploadVoice, 33 | sendImageMessage, 34 | sendVoiceMessage, 35 | sendFlashImageMessage, 36 | sendNudge, 37 | } = require('./src/sendMessage'); 38 | const ws = require('./src/ws'); 39 | 40 | const { 41 | getFriendList, 42 | getGroupList, 43 | getBotProfile, 44 | getFriendProfile, 45 | getMemberProfile, 46 | getMessageById, 47 | registerCommand, 48 | sendCommand, 49 | getManagers, 50 | botInvitedJoinGroupRequestHandler, 51 | quitGroup, 52 | handleNewFriendRequest, 53 | deleteFriend, 54 | getRoamingMessages, 55 | } = require('./src/manage'); 56 | 57 | const group = require('./src/group'); 58 | 59 | const { 60 | uploadFileAndSend, 61 | getGroupFileList, 62 | getGroupFileInfo, 63 | renameGroupFile, 64 | moveGroupFile, 65 | deleteGroupFile, 66 | makeDir, 67 | } = require('./src/fileUtility'); 68 | 69 | /** 70 | * @typedef MessageResponse 71 | * @property { number } code 72 | * @property { string } msg 73 | * @property { number } messageId 74 | * @property { () => Promise } recall 75 | * 76 | * @typedef { Promise } RecallableMessage 77 | */ 78 | /** 79 | * @typedef { import('./src/typedef').Buffer } Buffer 80 | * @typedef { import('./src/typedef').ReadStream } ReadStream 81 | * @typedef { import('./src/typedef').httpApiResponse } httpApiResponse 82 | * @typedef { import('./src/typedef').MessageChain } MessageChain 83 | * @typedef { import('./src/typedef').message } message 84 | * @typedef { import('./src/typedef').UserInfo } UserInfo 85 | * @typedef { import('./src/typedef').GroupMember } GroupMember 86 | * @typedef { import('./src/typedef').Friend } Friend 87 | * @typedef { import('./src/typedef').GroupPermissionInfo } GroupPermissionInfo 88 | * @typedef { import('./src/typedef').GroupInfo } GroupInfo 89 | * @typedef { import('./src/typedef').GroupFile } GroupFile 90 | * @typedef { import('./src/target').MessageTarget } MessageTarget 91 | * @typedef { import('./src/target').GroupTarget } GroupTarget 92 | * @typedef { import('./src/events').EventMap } EventMap 93 | * @typedef { import('./src/events').AllEventMap } AllEventMap 94 | */ 95 | /** 96 | * @namespace NodeMirai 97 | */ 98 | class NodeMirai { 99 | static MessageComponent = MessageComponent 100 | static Target = Target 101 | /** 102 | * @typedef { Object } BotConfig 103 | * @property { string } host http-api 服务的地址 104 | * @property { string } verifyKey http-api 服务的verifyKey 105 | * @property { number } qq bot 的 qq 号 106 | * @property { boolean } [enableWebsocket] 使用 ws 来获取消息和事件推送 107 | * @property { boolean } [wsOnly] 完全使用 ws 来收发消息,为 true 时覆盖 enableWebsocket 且无需调用 verify 108 | * @property { number } [syncId] wsOnly 模式下用于标记 server 主动推送的消息 109 | * @property { number } [interval] 拉取消息的周期(ms), 默认为200 110 | * @property { string } [authKey] (Deprecated) http-api 1.x 版本的authKey 111 | */ 112 | /** 113 | * Create a NodeMirai bot 114 | * @constructor 115 | * @param { BotConfig } config bot config 116 | * @param { string } config.host http-api 服务的地址 117 | * @param { string } config.verifyKey http-api 服务的verifyKey 118 | * @param { number } config.qq bot 的 qq 号 119 | * @param { boolean } [config.enableWebsocket] 使用 ws 来获取消息和事件推送 120 | * @param { boolean } [config.wsOnly] 完全使用 ws 来收发消息,为 true 时覆盖 enableWebsocket 且无需调用 verify 121 | * @param { number } [config.syncId] wsOnly 模式下用于标记 server 主动推送的消息 122 | * @param { number } [config.interval] 拉取消息的周期(ms), 默认为200 123 | * @param { string } [config.authKey] (Deprecated) http-api 1.x 版本的authKey 124 | */ 125 | constructor ({ 126 | host, 127 | verifyKey, 128 | qq, 129 | enableWebsocket = false, 130 | wsOnly = false, 131 | syncId = -1, 132 | interval = 200, 133 | authKey, 134 | }) { 135 | this.host = host; 136 | // support 1.x authKey 137 | this.verifyKey = verifyKey || authKey; 138 | this.qq = qq; 139 | this.interval = interval; 140 | this.signal = new Signal(); 141 | this.eventListeners = { 142 | message: [], 143 | }; 144 | for (let event in events) { 145 | this.eventListeners[events[event]] = []; 146 | } 147 | /** 148 | * @type { string[] } 149 | */ 150 | this.types = []; 151 | 152 | this.wsOnly = wsOnly; 153 | this.syncId = syncId; 154 | this.enableWebsocket = wsOnly || enableWebsocket; 155 | /** 156 | * @type { WebSocket | null } 157 | */ 158 | this.wsHost = null; 159 | this.plugins = []; 160 | this._is_mah_v1_ = false; 161 | this.mahVersion = '2.0.0'; 162 | checkMAHVersion(this).then(isV1 => { 163 | this._is_mah_v1_ = isV1; 164 | this.auth(); 165 | }); 166 | } 167 | 168 | /** 169 | * @method getBotList 170 | * @returns { number[] } 171 | */ 172 | async getBotList () { 173 | if (semver.lt(this.mahVersion, '2.6.0')) throw new Error('The getBotList API requires mah version >= 2.6.0'); 174 | if (this.wsOnly) return ws.send({ command: 'botList' }); 175 | return axios.get(`${this.host}/botList`).then(({ data }) => { 176 | if (data.code === 0) return data.data; 177 | return data; 178 | }); 179 | } 180 | 181 | /** 182 | * @method auth 183 | * @description Bot 认证, 获取 sessionKey 184 | * @returns { Promise } 185 | */ 186 | async auth () { 187 | if (this.enableWebsocket && !this._is_mah_v1_) { 188 | this.wsHost = new WebSocket(`${this.host.replace('http', 'ws')}/all?verifyKey=${this.verifyKey}&qq=${this.qq}`); 189 | if (this.wsOnly) { 190 | // skip binding sessionKey 191 | this.signal.trigger('authed'); 192 | this.signal.trigger('verified'); 193 | this.startListeningEvents(); 194 | ws.init(this.wsHost, this.syncId, this); 195 | return { 196 | code: 0, 197 | msg: 'authed', 198 | }; 199 | } 200 | } 201 | return init(this.host, this.verifyKey, this._is_mah_v1_).then(data => { 202 | const { code, session } = data; 203 | if (code !== 0) { 204 | console.error('Failed @ auth: Invalid auth key'); 205 | // process.exit(1); 206 | return { code, session }; 207 | } 208 | /** 209 | * @type { string } 210 | */ 211 | this.sessionKey = session; 212 | this.signal.trigger('authed'); 213 | this.startListeningEvents(); 214 | return { code, session }; 215 | }).catch((code) => { 216 | console.error('init error with code', code); 217 | // console.error('Failed @ auth: Invalid host'); 218 | // process.exit(1); 219 | return { 220 | code: 2, 221 | msg: 'Invalid host', 222 | }; 223 | }); 224 | } 225 | 226 | /** 227 | * @method verify 228 | * @description 校验 sessionKey, 必须在 authed 事件后进行 229 | * @returns { Promise } 230 | */ 231 | async verify () { 232 | if (this.wsOnly) return; 233 | return verify(this.host, this.sessionKey, this.qq, this._is_mah_v1_).then(({ code, msg }) => { 234 | if (code !== 0) { 235 | console.error('Failed @ verify: Invalid session key'); 236 | // process.exit(1); 237 | return { code, msg }; 238 | } 239 | this.signal.trigger('verified'); 240 | return { code, msg }; 241 | }); 242 | } 243 | 244 | /** 245 | * @method release 246 | * @description 释放 sessionKey 247 | * @returns { Promise } 248 | */ 249 | async release () { 250 | return release(this.host, this.sessionKey, this.qq).then(({ code, msg }) => { 251 | if (code !== 0) { 252 | console.error('Failed @ release: Invalid session key'); 253 | return { code, msg }; 254 | } 255 | this.signal.trigger('released'); 256 | return { code, msg }; 257 | }); 258 | } 259 | 260 | /** 261 | * @method fetchMessage 262 | * @param { number } count 263 | * @returns { Promise } 264 | */ 265 | async fetchMessage (count = 10) { 266 | return fetchMessage(this.host, this.sessionKey, count).catch(e => { 267 | console.error('Unknown error @ fetchMessage:', e.message); 268 | return []; 269 | // process.exit(1); 270 | }); 271 | } 272 | 273 | /** 274 | * @method getRoamingMessages 275 | * @description 获取漫游消息 276 | * @param { number } timeStart 起始时间, UTC+8 时间戳, 单位为秒. 可以为 0, 即表示从可以获取的最早的消息起. 负数将会被看是 0 277 | * @param { number } timeEnd 结束时间, 低于 `timeStart` 的值将会被看作是 `timeStart` 的值. 最大支持 `Number.MAX_SAFE_INTEGER` 278 | * @param { number } target 好友 qq, 目前仅支持好友消息漫游 279 | * @returns { message[] } 280 | */ 281 | async getRoamingMessages (timeStart, timeEnd, target) { 282 | return getRoamingMessages({ 283 | sessionKey: this.sessionKey, 284 | host: this.host, 285 | timeStart, 286 | timeEnd, 287 | target, 288 | wsOnly: this.wsOnly, 289 | }); 290 | } 291 | 292 | /** 293 | * @method NodeMirai#sendFriendMessage 294 | * @description 发送好友消息 295 | * @param { string | MessageChain[] } messageChain MessageChain 数组 296 | * @param { number } target 发送对象的 qq 号 297 | * @returns { RecallableMessage } 298 | */ 299 | async sendFriendMessage (messageChain, target) { 300 | return sendFriendMessage({ messageChain, target }, this); 301 | } 302 | /** 303 | * @method NodeMirai#sendGroupMessage 304 | * @description 发送群组消息 305 | * @param { string | MessageChain[] } messageChain MessageChain 数组 306 | * @param { number } group 发送群组的群号 307 | * @returns { RecallableMessage } 308 | */ 309 | async sendGroupMessage (messageChain, group) { 310 | return sendGroupMessage({ 311 | messageChain: messageChain, 312 | target: group, 313 | }, this); 314 | } 315 | /** 316 | * @method NodeMirai#sendTempMessage 317 | * @description 发送临时消息 318 | * @param { string | MessageChain[] } messageChain MessageChain 数组 319 | * @param { number } qq 临时消息发送对象 QQ 号 320 | * @param { number } group 所在群号 321 | * @returns { RecallableMessage } 322 | */ 323 | async sendTempMessage (messageChain, qq, group) { 324 | // 兼容旧格式:高 32 位为群号,低 32 位为 QQ 号 325 | if (!group) 326 | return sendTempMessage({ 327 | messageChain: messageChain, 328 | qq: (qq & 0xFFFFFFFF), 329 | group: ((qq >> 32) & 0xFFFFFFFF), 330 | }, this); 331 | else 332 | return sendTempMessage({ 333 | messageChain: messageChain, 334 | qq, 335 | group, 336 | }, this); 337 | } 338 | /** 339 | * @method NodeMirai#sendImageMessage 340 | * @param { string | Buffer | ReadStream } url 图片所在路径 341 | * @param { message | MessageTarget } target 发送目标对象 342 | * @returns { RecallableMessage } 343 | */ 344 | async sendImageMessage (url, target) { 345 | switch (target.type) { 346 | case 'FriendMessage': 347 | return sendImageMessage({ 348 | url, 349 | qq: target.sender.id, 350 | }, this); 351 | case 'GroupMessage': 352 | return sendImageMessage({ 353 | url, 354 | group: target.sender.group.id, 355 | }, this); 356 | default: 357 | console.error('Error @ sendImageMessage: unknown target type'); 358 | } 359 | } 360 | /** 361 | * @method NodeMirai#sendVoiceMessage 362 | * @param { string | Buffer | ReadStream } url 语音所在路径 363 | * @param { GroupTarget } target 发送目标对象(目前仅支持群组) 364 | * @returns { RecallableMessage } 365 | */ 366 | async sendVoiceMessage (url, target) { 367 | if (target.type !== 'GroupMessage') 368 | console.error('Error @ sendVoiceMessage: only support send voice to group'); 369 | 370 | return sendVoiceMessage({ 371 | url, 372 | group: target.sender.group.id, 373 | }, this); 374 | } 375 | 376 | /** 377 | * @method NodeMirai#sendFlashImageMessage 378 | * @param { string | Buffer | ReadStream } url 图片所在路径 379 | * @param { message | MessageTarget } target 发送目标对象 380 | * @returns { RecallableMessage } 381 | */ 382 | async sendFlashImageMessage (url, target) { 383 | switch (target.type) { 384 | case 'FriendMessage': 385 | return sendFlashImageMessage({ 386 | url, 387 | qq: target.sender.id, 388 | }, this); 389 | case 'GroupMessage': 390 | return sendFlashImageMessage({ 391 | url, 392 | group: target.sender.group.id, 393 | }, this); 394 | default: 395 | console.error('Error @ sendFlashImageMessage: unknown target type'); 396 | } 397 | } 398 | /** 399 | * @method NodeMirai#uploadImage 400 | * @param { string | Buffer | ReadStream } url 图片所在路径 401 | * @param { message | MessageTarget } target 发送目标对象 402 | * @returns {Promise<{ 403 | * imageId: string, 404 | * url: string 405 | * }>} 406 | */ 407 | async uploadImage (url, target) { 408 | let type; 409 | switch (target.type) { 410 | case 'FriendMessage': 411 | type = 'friend'; 412 | break; 413 | case 'GroupMessage': 414 | type = 'group'; 415 | break; 416 | case 'TempMessage': 417 | type = 'temp'; 418 | break; 419 | default: 420 | console.error('Error @ uploadImage: unknown target type'); 421 | } 422 | return uploadImage({ 423 | url, 424 | type, 425 | }, this); 426 | } 427 | 428 | 429 | /** 430 | * @method NodeMirai#uploadVoice 431 | * @param { string | Buffer | ReadStream } url 声音所在路径 432 | * @returns {Promise<{ 433 | * voiceId: string, 434 | * url: string 435 | * }>} 436 | */ 437 | async uploadVoice (url) { 438 | return uploadVoice({ 439 | url, 440 | type: 'group', 441 | }, this); 442 | } 443 | 444 | /** 445 | * @method NodeMirai#sendMessage 446 | * @description 发送消息给指定好友或群组 447 | * @param { MessageChain[]|string } message 要发送的消息 448 | * @param { message | MessageTarget } target 发送目标对象 449 | * @returns { RecallableMessage } 450 | */ 451 | async sendMessage (message, target) { 452 | switch (target.type) { 453 | case 'FriendMessage': 454 | return this.sendFriendMessage(message, target.sender.id); 455 | case 'GroupMessage': 456 | return this.sendGroupMessage(message, target.sender.group.id); 457 | case 'TempMessage': 458 | return this.sendTempMessage(message, target.sender.id, target.sender.group.id); 459 | default: 460 | console.error('Invalid target @ sendMessage'); 461 | } 462 | } 463 | 464 | /** 465 | * @method NodeMirai#sendQuotedFriendMessage 466 | * @description 发送带引用的好友消息 467 | * @param { MessageChain[] } message MessageChain 数组 468 | * @param { number } target 发送对象的 qq 号 469 | * @param { number } quote 引用的 Message 的 id 470 | * @returns { RecallableMessage } 471 | */ 472 | async sendQuotedFriendMessage (message, target, quote) { 473 | return sendQuotedFriendMessage({ 474 | messageChain: message, 475 | target, 476 | quote, 477 | }, this); 478 | } 479 | /** 480 | * @method NodeMirai#sendQuotedGroupMessage 481 | * @description 发送带引用的群组消息 482 | * @param { MessageChain[] } message MessageChain 数组 483 | * @param { number } qq 发送群组的群号 484 | * @param { number} quote 引用的 Message 的 id 485 | * @returns { RecallableMessage } 486 | */ 487 | async sendQuotedGroupMessage (message, target, quote) { 488 | return sendQuotedGroupMessage({ 489 | messageChain: message, 490 | target, quote, 491 | }, this); 492 | } 493 | /** 494 | * @method NodeMirai#sendQuotedTempMessage 495 | * @description 发送带引用的临时消息 496 | * @param { MessageChain[] } message MessageChain 数组 497 | * @param { number } qq 临时消息发送对象 QQ 号 498 | * @param { number } group 所在群号 499 | * @param { number} quote 引用的 Message 的 id 500 | * @returns { RecallableMessage } 501 | */ 502 | async sendQuotedTempMessage (message, qq, group, quote) { 503 | // 兼容旧格式:高 32 位为群号,低 32 位为 QQ 号 504 | // 若使用旧 API 格式,则 group 位置的值实为 quote 505 | if (!quote) 506 | return sendQuotedTempMessage({ 507 | messageChain: message, 508 | qq: (qq & 0xFFFFFFFF), 509 | group: ((qq >> 32) & 0xFFFFFFFF), 510 | quote: group, 511 | sessionKey: this.sessionKey, 512 | host: this.host, 513 | }, this); 514 | else 515 | return sendQuotedTempMessage({ 516 | messageChain: message, 517 | qq, 518 | group, 519 | quote, 520 | sessionKey: this.sessionKey, 521 | host: this.host, 522 | }, this); 523 | } 524 | 525 | /** 526 | * @method NodeMirai#sendQuotedMessage 527 | * @description 发送引用消息 528 | * @param { MessageChain[]|string } message 要发送的消息 529 | * @param { message } target 发送目标对象 530 | * @returns { RecallableMessage } 531 | */ 532 | async sendQuotedMessage (message, target) { 533 | let quote = target.messageChain[0].type === 'Source' ? target.messageChain[0].id : undefined; 534 | // messageId 可以是负数 535 | if (quote === undefined) throw new Error('Cannot get messageId from target'); 536 | // console.log(target.type, quote); 537 | switch (target.type) { 538 | case 'FriendMessage': 539 | return await this.sendQuotedFriendMessage(message, target.sender.id, quote); 540 | case 'GroupMessage': 541 | return await this.sendQuotedGroupMessage(message, target.sender.group.id, quote); 542 | case 'TempMessage': 543 | return await this.sendQuotedTempMessage(message, target.sender.id, target.sender.group.id, quote); 544 | default: 545 | console.error('Invalid target @ sendQuotedMessage'); 546 | // process.exit(1); 547 | } 548 | } 549 | 550 | /** 551 | * @method NodeMirai#sendNudge 552 | * @description 发送戳一戳消息, 未提供群号时为好友消息 553 | * @param { number } qq 好友或群员的QQ 554 | * @param { number } group 群号 555 | */ 556 | async sendNudge (qq, group) { 557 | if (group) return sendNudge(Object.assign({}, this, { target: qq, subject: group, kind: 'Group' })) 558 | else { 559 | // TODO: Stranger is not supported. Expect returns an error if `qq` is not bot's friend 560 | return sendNudge(Object.assign({}, this, { target: qq, subject: qq, kind: 'Friend' })); 561 | } 562 | } 563 | 564 | /** 565 | * @method NodeMirai#reply 566 | * @description 回复一条消息, sendMessage 的别名方法 567 | * @param { MessageChain[]|string } replyMsg 回复的内容 568 | * @param { message | MessageTarget } srcMsg 源消息 569 | * @param { boolean } [quote] 是否引用源消息 570 | * @returns { RecallableMessage } 571 | */ 572 | reply (replyMsg, srcMsg, quote = false) { 573 | const replyMessage = typeof replyMsg === 'string' ? [Plain(replyMsg)] : replyMsg; 574 | if (quote) return this.sendQuotedMessage(replyMessage, srcMsg); 575 | return this.sendMessage(replyMessage, srcMsg); 576 | } 577 | /** 578 | * @method NodeMirai#quoteReply 579 | * @description 引用回复一条消息, sendQuotedMessage 的别名方法 580 | * @param { MessageChain[]|string } replyMsg 回复的内容 581 | * @param { message | MessageTarget } srcMsg 源消息 582 | * @returns { RecallableMessage } 583 | */ 584 | quoteReply (replyMsg, srcMsg) { 585 | const replyMessage = typeof replyMsg === 'string' ? [Plain(replyMsg)] : replyMsg; 586 | return this.sendQuotedMessage(replyMessage, srcMsg); 587 | } 588 | 589 | /** 590 | * @method NodeMirai#recall 591 | * @description 撤回一条消息 592 | * @param { message|number } msg 要撤回的消息或消息 id 593 | * @param { number } [targetId] (mah >= v2.6.0)撤回消息为消息 id 时, 需提供好友 qq 或群号 594 | * @returns { Promise } 595 | */ 596 | async recall (msg, targetId) { 597 | let messageId = msg 598 | if (msg.messageId) messageId = msg.messageId 599 | if (msg.messageChain && msg.messageChain[0] && msg.messageChain[0].id) { 600 | messageId = msg.messageChain[0].id 601 | } 602 | if (typeof messageId !== 'number') throw new Error(`Cannot get messageId from ${msg}`) 603 | /** 604 | * API changed since mah v2.6.0 605 | * @see https://github.com/project-mirai/mirai-api-http/releases/tag/v2.6.0 606 | * @see https://github.com/project-mirai/mirai-api-http/commit/8ba96b5dd7362ef83bde9f210d43d319319bc896 607 | */ 608 | if (semver.gte(this.mahVersion, '2.6.0')) { 609 | const target = msg.sender 610 | ? msg.sender.group 611 | ? msg.sender.group.id 612 | : msg.sender.id 613 | : targetId 614 | if (typeof target !== 'number') { 615 | throw new Error(`Cannot get targetId. Recall by messageId must also pass targetId since mah 2.6.0`) 616 | } 617 | return recall({ 618 | target, 619 | messageId, 620 | sessionKey: this.sessionKey, 621 | host: this.host, 622 | wsOnly: this.wsOnly 623 | }); 624 | } else { 625 | return recall({ 626 | target: messageId, 627 | sessionKey: this.sessionKey, 628 | host: this.host, 629 | wsOnly: this.wsOnly, 630 | }); 631 | } 632 | } 633 | 634 | /** 635 | * @method NodeMirai#getFriendList 636 | * @description 获取 bot 的好友列表 637 | * @returns { Promise } 638 | */ 639 | getFriendList () { 640 | return getFriendList({ 641 | host: this.host, 642 | sessionKey: this.sessionKey, 643 | wsOnly: this.wsOnly, 644 | }); 645 | } 646 | /** 647 | * @method NodeMirai#getGroupList 648 | * @description 获取 bot 的群组列表 649 | * @returns { Promise } 650 | */ 651 | getGroupList () { 652 | return getGroupList({ 653 | host: this.host, 654 | sessionKey: this.sessionKey, 655 | wsOnly: this.wsOnly, 656 | }); 657 | } 658 | /** 659 | * @method NodeMirai#getBotProfile 660 | * @description 获取 bot 资料 661 | * @returns { Promise } 662 | */ 663 | getBotProfile () { 664 | return getBotProfile(this); 665 | } 666 | /** 667 | * @method NodeMirai#getFriendProfile 668 | * @param { number } qq 好友的 QQ 号 669 | * @returns { Promise } 670 | */ 671 | getFriendProfile (qq) { 672 | return getFriendProfile(Object.assign({}, this, { qq })); 673 | } 674 | /** 675 | * @method NodeMirai#getGroupMemberProfile 676 | * @description 获取群员的个人资料 677 | * @param { number } group 群号 678 | * @param { number } qq 群员的 QQ 号 679 | * @returns { Promise } 680 | */ 681 | getGroupMemberProfile (group, qq) { 682 | return getMemberProfile(Object.assign({}, this, { group, qq })); 683 | } 684 | /** 685 | * @method NodeMirai#getMessageById 686 | * @description 根据消息 id 获取消息内容 687 | * @param { number } messageId 指定的消息 id 688 | * @param { number } target 好友 QQ 或群号 689 | * @return { Promise<{ code: 0|5, data: message }> } 690 | */ 691 | getMessageById (messageId, target) { 692 | return getMessageById(Object.assign({}, this, { messageId, target })); 693 | } 694 | 695 | /** 696 | * @method NodeMirai#getGroupMemberList 697 | * @description 获取指定群的成员名单 698 | * @param { number } target 指定的群号 699 | * @returns { Promise } 700 | */ 701 | getGroupMemberList (target) { 702 | return group.getMemberList(Object.assign({}, this, { target })); 703 | } 704 | /** 705 | * @method NodeMirai#setGroupMute 706 | * @description 禁言一位群员(需有相应权限) 707 | * @param { number } target 群号 708 | * @param { number } memberId 群员的 qq 号 709 | * @param { number } time 禁言时间(秒) 710 | * @returns { Promise } 711 | */ 712 | setGroupMute (target, memberId, time = 600) { 713 | return group.setMute(Object.assign({}, this, { 714 | target, memberId, time, 715 | })); 716 | } 717 | /** 718 | * @method NodeMirai#setGroupUnmute 719 | * @description 解除一位群员的禁言状态 720 | * @param { number } target 群号 721 | * @param { number } memberId 群员的 qq 号 722 | * @returns { Promise } 723 | */ 724 | setGroupUnmute (target, memberId) { 725 | return group.setUnmute(Object.assign({}, this, { 726 | target, memberId, 727 | })); 728 | } 729 | /** 730 | * @method NodeMirai#setGroupMuteAll 731 | * @description 设置全体禁言 732 | * @param { number } target 群号 733 | * @returns { Promise } 734 | */ 735 | setGroupMuteAll (target) { 736 | return group.setMuteAll(Object.assign({}, this, { target })); 737 | } 738 | /** 739 | * @method NodeMirai#setGroupUnmuteAll 740 | * @description 解除全体禁言 741 | * @param { number } target 群号 742 | * @returns { Promise } 743 | */ 744 | setGroupUnmuteAll (target) { 745 | return group.setUnmuteAll(Object.assign({}, this, { target })); 746 | } 747 | /** 748 | * @method NodeMirai#setGroupKick 749 | * @description 移除群成员 750 | * @param { number } target 群号 751 | * @param { number } memberId 群员的 qq 号 752 | * @param { string } msg 信息 753 | * @returns { Promise } 754 | */ 755 | setGroupKick (target, memberId, msg = '您已被移出群聊') { 756 | return group.setKick(Object.assign({}, this, { 757 | target, memberId, msg, 758 | })); 759 | } 760 | /** 761 | * @method NodeMirai#setGroupConfig 762 | * @description 修改群设置 763 | * @param { number } target 群号 764 | * @param { Partial } config 设置 765 | * @returns { Promise } 766 | */ 767 | setGroupConfig (target, config) { 768 | return group.setConfig(Object.assign({}, this, { 769 | target, config, 770 | })); 771 | } 772 | /** 773 | * @method NodeMirai#setEssence 774 | * @description 设置群精华消息 775 | * @param { number | string | GroupTarget } target 要设置的群 776 | * @param { number } id 精华消息 ID 777 | * @returns { Promise } 778 | */ 779 | setEssence(target, id) { 780 | const { host, sessionKey } = this; 781 | const realTarget = (typeof target === 'number') || (typeof target === 'string') 782 | ? target 783 | : target.sender.group.id; 784 | return group.setEssence({ 785 | target: realTarget, 786 | id, 787 | host, 788 | sessionKey, 789 | wsOnly: this.wsOnly, 790 | }); 791 | } 792 | /** 793 | * @method NodeMirai#getGroupConfig 794 | * @description 获取群设置 795 | * @param { number } target 群号 796 | * @returns { Promise } 797 | */ 798 | getGroupConfig (target) { 799 | return group.getConfig(Object.assign({}, this, target)); 800 | } 801 | /** 802 | * @method NodeMirai#setGroupMemberInfo 803 | * @description 设置群成员信息 804 | * @param { number } target 群号 805 | * @param { number } memberId 群员 qq 号 806 | * @param { Partial } info 信息 807 | * @returns { Promise } 808 | */ 809 | setGroupMemberInfo (target, memberId, info) { 810 | return group.setMemberInfo(Object.assign({}, this, { 811 | target, memberId, info, 812 | })); 813 | } 814 | /** 815 | * @method NodeMirai#getGroupMemberInfo 816 | * @description 获取群成员信息 817 | * @param { number } target 群号 818 | * @param { number } memberId 群员 qq 号 819 | * @returns { Promise } 820 | */ 821 | getGroupMemberInfo (target, memberId) { 822 | return group.getMemberInfo(Object.assign({}, this, { 823 | target, memberId, 824 | })); 825 | } 826 | 827 | /** 828 | * @method NodeMirai#quit 829 | * @description BOT 主动离群 830 | * @param { number } target 要离开的群的群号 831 | * @returns { Promise } 832 | */ 833 | quit(target) { 834 | return quitGroup(Object.assign({}, this, { target })); 835 | } 836 | 837 | /** 838 | * @method NodeMirai#handleMemberJoinRequest 839 | * @description 处理用户入群申请 840 | * @param { number } eventId 入群事件 (memberJoinRequest) ID 841 | * @param { number } fromId 申请入群人 QQ 号 842 | * @param { number } groupId 申请入群群号 843 | * @param { 0|1|2|3|4 } operate 响应操作,0同意,1拒绝,2忽略,3拒绝并拉黑,4忽略并拉黑 844 | * @param { string } message 回复的消息 845 | * @returns { Promise } 846 | */ 847 | handleMemberJoinRequest (eventId, fromId, groupId, operate, message = "") { 848 | return group.handleMemberJoinRequest({ 849 | eventId, 850 | fromId, 851 | groupId, 852 | operate, 853 | message, 854 | host: this.host, 855 | sessionKey: this.sessionKey, 856 | wsOnly: this.wsOnly, 857 | }); 858 | } 859 | 860 | /** 861 | * @method NodeMirai#handleBotInvitedJoinGroupRequest 862 | * @description 处理 BOT 被邀请入群的申请 863 | * @param { number } eventId 被邀请入群事件 (botInvitedJoinGroupRequest) ID 864 | * @param { number } fromId 邀请人群者的 QQ 号 865 | * @param { number } groupId 被邀请进入群的群号 866 | * @param { 0|1 } operate 响应的操作类型, 0同意邀请,1拒绝邀请 867 | * @param { string } message 回复的信息 868 | * @returns { Promise } 869 | */ 870 | handleBotInvitedJoinGroupRequest(eventId, fromId, groupId, operate, message = "") { 871 | // 由于方法是单独引入的,所以使用 [event]Handler 而不是 handle[Event] 作为函数名 872 | return botInvitedJoinGroupRequestHandler({ 873 | eventId, 874 | fromId, 875 | groupId, 876 | operate, 877 | message, 878 | host: this.host, 879 | sessionKey: this.sessionKey, 880 | wsOnly: this.wsOnly, 881 | }); 882 | } 883 | 884 | /** 885 | * @method NodeMirai#handleNewFriendRequest 886 | * @description 处理好友申请 887 | * @param { number } eventId 好友申请事件 (newFriendRequest) ID 888 | * @param { number } fromId 申请人 QQ 号 889 | * @param { number } groupId 申请人如果通过某个群添加好友,该项为该群群号;否则为0 890 | * @param { 0|1|2 } operate 响应操作,0同意,1拒绝,2拒绝并拉黑 891 | * @param { string } message 回复的消息 892 | * @returns { Promise } 893 | */ 894 | handleNewFriendRequest (eventId, fromId, groupId, operate, message = "") { 895 | return handleNewFriendRequest({ 896 | eventId, 897 | fromId, 898 | groupId, 899 | operate, 900 | message, 901 | host: this.host, 902 | sessionKey: this.sessionKey, 903 | wsOnly: this.wsOnly, 904 | }); 905 | } 906 | 907 | /** 908 | * @method NodeMirai#uploadFileAndSend 909 | * @description 上传(群)文件并发送 910 | * @param { string | Buffer | ReadStream } url 文件所在路径或 URL 911 | * @param { string | GroupFile } path 文件要上传到群文件中的位置(路径) 912 | * @param { number | GroupTarget } [target] 要发送文件的目标 913 | * @returns { Promise } 914 | */ 915 | uploadFileAndSend(url, path, target) { 916 | const { sessionKey, host } = this; 917 | if (!target && typeof path === 'object') { 918 | target = path.contact.id; 919 | } 920 | const realTarget = (typeof target === 'number') || (typeof target === 'string') 921 | ? target 922 | : target.sender.group.id; 923 | return uploadFileAndSend({ 924 | url, 925 | path, 926 | target: realTarget, 927 | sessionKey, 928 | host, 929 | isV1: this._is_mah_v1_, 930 | wsOnly: this.wsOnly, 931 | }); 932 | } 933 | 934 | 935 | /** 936 | * @method NodeMirai#getGroupFileList 937 | * @description 获取群文件指定路径下的文件列表 938 | * @param { GroupFile | string | number } dir - - `GroupFile|string` 要获取的群文件路径对象, 使用 `string` 结果可能不准确 939 | * - `number` 获取指定群的群文件根目录 `bot.getGroupFileList(groupId)` 940 | * @param { number | string | GroupTarget } [target] 要获取的群号, `dir` 为 `File` 时可不提供 941 | * @param { boolean } [withDownloadInfo] 是否携带下载信息, 无必要不要携带 942 | * @returns { Promise } 943 | * 944 | */ 945 | getGroupFileList(dir, target, withDownloadInfo) { 946 | const { sessionKey, host } = this; 947 | if (typeof dir === 'object') { 948 | if (!target) target = dir.contact.id; 949 | if (dir.isFile) console.warn(`Warning: Getting list of a file will get empty returns`); 950 | } 951 | // 兼容写法: getGroupFileList(groupid) 返回指定群的根目录 952 | if (typeof dir === 'number' || typeof dir === 'bigint') { 953 | [dir, target] = ['/', dir]; 954 | } 955 | const realTarget = (typeof target === 'number') || (typeof target === 'string') 956 | ? target 957 | : target.sender.group.id; 958 | return getGroupFileList({ 959 | target: realTarget, 960 | dir, 961 | sessionKey, 962 | host, 963 | withDownloadInfo, 964 | isV1: this._is_mah_v1_, 965 | wsOnly: this.wsOnly, 966 | }); 967 | } 968 | 969 | /** 970 | * @method NodeMirai#getGroupFileInfo 971 | * @description 获取群文件指定详细信息 972 | * @param { string | GroupFile } id 文件唯一 ID 或文件对象 973 | * @param { number | string | GroupTarget } [target] 要获取的群号 974 | * @param { boolean } [withDownloadInfo] 是否携带下载信息, 无必要不要携带 975 | * @returns { Promise } 976 | */ 977 | getGroupFileInfo(id, target, withDownloadInfo) { 978 | const { sessionKey, host } = this; 979 | if (!target && typeof id === 'object') { 980 | target = id.contact.id; 981 | } 982 | const realTarget = (typeof target === 'number') || (typeof target === 'string') 983 | ? target 984 | : target.sender.group.id; 985 | return getGroupFileInfo({ 986 | target: realTarget, 987 | id, 988 | sessionKey, 989 | host, 990 | withDownloadInfo, 991 | isV1: this._is_mah_v1_, 992 | wsOnly: this.wsOnly, 993 | }); 994 | } 995 | 996 | /** 997 | * @method NodeMirai#renameGroupFile 998 | * @description 重命名指定群文件 999 | * @param { string | GroupFile } id 要重命名的文件唯一 ID 或文件对象 1000 | * @param { string } rename 文件的新名称 1001 | * @param { number | string | GroupTarget } [target] 目标群号 1002 | * @returns { Promise } 1003 | */ 1004 | renameGroupFile(id, rename, target) { 1005 | const { sessionKey, host } = this; 1006 | if (!target && typeof id === 'object') { 1007 | target = id.contact.id; 1008 | } 1009 | const realTarget = (typeof target === 'number') || (typeof target === 'string') 1010 | ? target 1011 | : target.sender.group.id; 1012 | return renameGroupFile({ 1013 | target: realTarget, 1014 | id, 1015 | rename, 1016 | sessionKey, 1017 | host, 1018 | isV1: this._is_mah_v1_, 1019 | wsOnly: this.wsOnly, 1020 | }); 1021 | } 1022 | 1023 | /** 1024 | * @method NodeMirai#moveGroupFile 1025 | * @description 移动指定群文件 1026 | * @param { string | GroupFile } id 要移动的文件唯一 ID 或文件对象 1027 | * @param { string | GroupFile } moveTo 文件的新路径或文件夹对象, 使用 `string` 可能结果不准确 1028 | * @param { number | string | GroupTarget } [target] 目标群号 1029 | * @returns { Promise } 1030 | */ 1031 | moveGroupFile(id, moveTo, target) { 1032 | const { sessionKey, host } = this; 1033 | if (!target && typeof moveTo === 'object') { 1034 | target = moveTo.contact.id; 1035 | } 1036 | const realTarget = (typeof target === 'number') || (typeof target === 'string') 1037 | ? target 1038 | : target.sender.group.id; 1039 | return moveGroupFile({ 1040 | target: realTarget, 1041 | id, 1042 | moveTo, 1043 | sessionKey, 1044 | host, 1045 | isV1: this._is_mah_v1_, 1046 | wsOnly: this.wsOnly, 1047 | }); 1048 | } 1049 | 1050 | /** 1051 | * @typedef { httpApiResponse & { data: GroupFile } } makeDirResponse 1052 | */ 1053 | /** 1054 | * @method NodeMirai#makeDir 1055 | * @description 创建文件夹 1056 | * @param { string | GroupFile | null } id 父目录id, 空串或null为根目录 1057 | * @param { string } directoryName 新建文件夹名 1058 | * @param { string | number } [target] 群号 1059 | * @returns { Promise } 1060 | */ 1061 | makeDir (id, directoryName, target) { 1062 | const { sessionKey, host } = this; 1063 | if (!target && typeof id === 'object' && id !== null) { 1064 | target = id.contact.id; 1065 | } 1066 | if (!target) { 1067 | console.warn(`Error: Expect providing a target if id is empty`); 1068 | } 1069 | return makeDir({ 1070 | sessionKey, 1071 | host, 1072 | id, 1073 | target, 1074 | directoryName, 1075 | isV1: this._is_mah_v1_, 1076 | wsOnly: this.wsOnly, 1077 | }); 1078 | } 1079 | 1080 | /** 1081 | * 删除指定群文件 1082 | * @param { string | GroupFile } id 要删除的文件唯一 ID 1083 | * @param { number | string | GroupTarget } [target] 目标群号 1084 | * @returns { Promise } 1085 | */ 1086 | deleteGroupFile(id, target) { 1087 | const { sessionKey, host } = this; 1088 | if (!target && typeof id === 'object') { 1089 | target = id.contact.id; 1090 | } 1091 | const realTarget = (typeof target === 'number') || (typeof target === 'string') 1092 | ? target 1093 | : target.sender.group.id; 1094 | return deleteGroupFile({ 1095 | target: realTarget, 1096 | id, 1097 | sessionKey, 1098 | host, 1099 | isV1: this._is_mah_v1_, 1100 | wsOnly: this.wsOnly, 1101 | }); 1102 | } 1103 | 1104 | /** 1105 | * @method NodeMirai#deleteFriend 1106 | * @description 删除好友 1107 | * @param { number } qq 好友的QQ号 1108 | * @returns { Promise } 1109 | */ 1110 | deleteFriend (qq) { 1111 | return deleteFriend(Object.assign({}, this, { target: qq })); 1112 | } 1113 | 1114 | getManagers () { 1115 | return getManagers({ 1116 | host: this.host, 1117 | verifyKey: this.verifyKey, 1118 | qq: this.qq, 1119 | }); 1120 | } 1121 | 1122 | getManager () { 1123 | return util.deprecate(this.getManagers, 'NodeMirai#getManager is deprecated, use getManagers instead'); 1124 | } 1125 | 1126 | // command 1127 | /** 1128 | * @method NodeMirai#registerCommand 1129 | * @param { Object } command 注册的 command 对象 1130 | * @param { string } command.name 1131 | * @param { string[] } command.alias 1132 | * @param { string } command.description 1133 | * @param { string } command.usage 1134 | * @returns { Promise } 1135 | */ 1136 | registerCommand (command) { 1137 | return registerCommand(Object.assign({ 1138 | host: this.host, 1139 | verifyKey: this.verifyKey, 1140 | }, command)); 1141 | } 1142 | /** 1143 | * @method NodeMirai#sendCommand 1144 | * @param { Object } command 发送的 command 对象 1145 | * @param { string } command.name 1146 | * @param { string[] } command.args 1147 | * @returns { Promise } 1148 | */ 1149 | sendCommand (command) { 1150 | return sendCommand(Object.assign({ 1151 | host: this.host, 1152 | verifyKey: this.verifyKey, 1153 | }, command)); 1154 | } 1155 | 1156 | // event listener 1157 | /** 1158 | * @method NodeMirai#on 1159 | * @description 事件监听 1160 | * @template { keyof AllEventMap } N 1161 | * @param { N } name 1162 | * @param { (message: AllEventMap[N], self?: NodeMirai) => void } callback 1163 | */ 1164 | on (name, callback) { 1165 | if (name === 'message') return this.onMessage(callback); 1166 | else if (name === 'command') return this.onCommand(callback); 1167 | else if (this.signal.signalList.includes(name)) return this.onSignal(name, callback); 1168 | return this.onEvent(name, callback); 1169 | } 1170 | /** 1171 | * @method NodeMirai#onSignal 1172 | * @description 订阅 authed, verified, 或 released 信号 1173 | * @param { "authed"|"verified"|"released" } signalName 信号 1174 | * @param { () => void } callback 回调 1175 | */ 1176 | onSignal (signalName, callback) { 1177 | return this.signal.on(signalName, callback); 1178 | } 1179 | /** 1180 | * @method NodeMirai#onMessage 1181 | * @description 订阅消息事件 1182 | * @param { (message: message, self?: NodeMirai) => void } callback 回调 1183 | */ 1184 | onMessage (callback) { 1185 | this.eventListeners.message.push(callback); 1186 | } 1187 | /** 1188 | * @template { keyof EventMap } E 1189 | * @method NodeMirai#onEvent 1190 | * @param { E } event 1191 | * @param { (event: EventMap[E], self?: NodeMirai) => void } callback 1192 | */ 1193 | onEvent (event, callback) { 1194 | if (!this.eventListeners[event]) this.eventListeners[event] = []; 1195 | this.eventListeners[event].push(callback); 1196 | } 1197 | onCommand (callback) { 1198 | const ws = new WebSocket(`${this.host.replace('http', 'ws')}/command?verifyKey=${this.verifyKey}`); 1199 | ws.on('message', message => { 1200 | callback(JSON.parse(message)); 1201 | }); 1202 | } 1203 | 1204 | /** 1205 | * @method NodeMirai#listen 1206 | * @description 启动事件监听 1207 | * @param { ["all"] | Array<"friend"|"group"|"temp"> } types 类型 1208 | */ 1209 | listen (...types) { 1210 | this.types = []; 1211 | if (types.includes('all')) { 1212 | this.types.push('FriendMessage', 'GroupMessage', 'TempMessage'); 1213 | return; 1214 | } 1215 | for (const type of types) { 1216 | switch (type) { 1217 | case 'group': this.types.push('GroupMessage'); break; 1218 | case 'friend': this.types.push('FriendMessage'); break; 1219 | case 'temp': this.types.push('TempMessage'); break; 1220 | default: 1221 | console.error('Invalid listen type. Type should be "all", "friend", "group" or "temp"'); 1222 | } 1223 | } 1224 | } 1225 | startListeningEvents () { 1226 | if (this.isEventListeningStarted) return; 1227 | this.isEventListeningStarted = true; 1228 | if (this.wsOnly) return; 1229 | if (this.enableWebsocket) { 1230 | this.onSignal('verified', () => { 1231 | if (!this.wsHost) { 1232 | const wsHost = `${this.host.replace('http', 'ws')}/all?sessionKey=${this.sessionKey}`; 1233 | this.wsHost = new WebSocket(wsHost); 1234 | } 1235 | this.wsHost.on('message', message => { 1236 | this.emitEventListener(JSON.parse(message)); 1237 | }); 1238 | }); 1239 | } 1240 | else setInterval(async () => { 1241 | const messages = await this.fetchMessage(10); 1242 | if (messages.length) { 1243 | messages.forEach(message => { 1244 | return this.emitEventListener(message); 1245 | }); 1246 | // } else if (messages.code) { 1247 | // console.error(`Error @ fetchMessage:\n\tCode: ${messages.code}\n\tMessage: ${messages.message || messages.msg || messages}`); 1248 | } 1249 | }, this.interval); 1250 | } 1251 | emitEventListener (messageResp) { 1252 | // No `code` or `code = 0` presents a success response 1253 | if (messageResp.code) { 1254 | console.error(`Error: bad response with code ${messageResp.code}: ${messageResp.msg}`); 1255 | return; 1256 | } 1257 | // get response.data for 2.x or message for 1.x 1258 | const message = messageResp.data || messageResp; 1259 | if (this.types.includes(message.type)) { 1260 | message.reply = msg => this.reply(msg, message); 1261 | message.quoteReply = msg => this.quoteReply(msg, message); 1262 | message.recall = () => this.recall(message); 1263 | for (let listener of this.eventListeners.message) { 1264 | listener(message, this); 1265 | } 1266 | } 1267 | else if (message.type in events) { 1268 | if (['NewFriendRequestEvent', 'BotInvitedJoinGroupRequestEvent', 'MemberJoinRequestEvent'].includes(message.type)) { 1269 | const self = this; 1270 | const args = [message.eventId, message.fromId, message.groupId]; 1271 | const eventHandler = message.type === 'NewFriendRequestEvent' 1272 | ? (...opt) => self.handleNewFriendRequest(...args, ...opt) 1273 | : message.type === 'BotInvitedJoinGroupRequestEvent' 1274 | ? (...opt) => self.handleBotInvitedJoinGroupRequest(...args, ...opt) 1275 | : (...opt) => self.handleMemberJoinRequest(...args, ...opt) 1276 | const methods = { 1277 | accept (msg) { 1278 | return eventHandler(0, msg); 1279 | }, 1280 | reject (msg) { 1281 | return eventHandler(1, msg); 1282 | }, 1283 | rejectAndBlock: null, 1284 | ignore: null, 1285 | ignoreAndBlock: null, 1286 | }; 1287 | if (message.type === 'NewFriendRequestEvent') { 1288 | methods.rejectAndBlock = (msg) => { 1289 | return self.handleNewFriendRequest(...args, 2, msg); 1290 | }; 1291 | } 1292 | if (message.type === 'MemberJoinRequestEvent') { 1293 | methods.ignore = msg => self.handleMemberJoinRequest(...args, 2, msg); 1294 | methods.rejectAndBlock = msg => self.handleMemberJoinRequest(...args, 3, msg); 1295 | methods.ignoreAndBlock = msg => self.handleMemberJoinRequest(...args, 4, msg); 1296 | } 1297 | Object.assign(message, methods); 1298 | } 1299 | for (let listener of this.eventListeners[events[message.type]]) { 1300 | listener(message, this); 1301 | } 1302 | } 1303 | } 1304 | 1305 | // plugins 1306 | // 这个插件系统需要大量改进 1307 | getPlugins () { 1308 | return this.plugins.map(i => i.name); 1309 | } 1310 | /** 1311 | * @method NodeMirai#use 1312 | * @description install plugin 1313 | * @param { object } plugin plugin config 1314 | * @param { string } plugin.name unique plugin name 1315 | * @param { string } [plugin.subscribe] subscribe event name 1316 | * @param { function } plugin.callback callback function 1317 | */ 1318 | use (plugin) { 1319 | 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.`); 1320 | if (!plugin.callback || typeof plugin.callback !== 'function') throw new Error('[NodeMirai] Invalid plugin callback. Plugin callback must be a function.'); 1321 | if (this.getPlugins().includes(plugin.name)) throw new Error(`[NodeMirai] Duplicate plugin name ${plugin.name}`); 1322 | this.plugins.push(plugin); 1323 | // TODO: support string[] 1324 | const event = typeof plugin.subscribe === 'string' ? plugin.subscribe : 'message'; 1325 | this.on(event, plugin.callback); 1326 | console.log(`[NodeMirai] Installed plugin [ ${plugin.name} ]`); 1327 | } 1328 | remove (pluginName) { 1329 | const pluginNames = this.getPlugins(); 1330 | if (pluginNames.includes(pluginName)) { 1331 | const plugin = this.plugins[pluginNames.indexOf(pluginName)]; 1332 | for (let event in this.eventListeners) { 1333 | for (let i in this.eventListeners[event]) { 1334 | if (this.eventListeners.message[i] === plugin.callback) { 1335 | this.eventListeners.message.splice(i, 1); 1336 | console.log(`[NodeMirai] Uninstalled plugin [ ${plugin.name} ]`); 1337 | } 1338 | } 1339 | } 1340 | } 1341 | } 1342 | } 1343 | 1344 | module.exports = NodeMirai; 1345 | --------------------------------------------------------------------------------