├── .eslintignore ├── .gitignore ├── .npmignore ├── .github ├── auto-merge.yml ├── workflows │ ├── automerge.yml │ ├── typescript.yml │ └── codeql-analysis.yml └── dependabot.yml ├── tsup.config.ts ├── src ├── Error.ts ├── core │ ├── avatar │ │ ├── getGroupAvatar.ts │ │ └── getUserAvatar.ts │ ├── listenStop.ts │ ├── release.ts │ ├── auth.ts │ ├── getFriendList.ts │ ├── bind.ts │ ├── getBotProfile.ts │ ├── muteAll.ts │ ├── quitGroup.ts │ ├── unmuteAll.ts │ ├── getGroupList.ts │ ├── removeFriend.ts │ ├── getUserProfile.ts │ ├── getFriendProfile.ts │ ├── unmute.ts │ ├── anno │ │ ├── deleteAnno.ts │ │ ├── publishAnno.ts │ │ └── getAnnoList.ts │ ├── getGroupConfig.ts │ ├── getMemberList.ts │ ├── setGroupConfig.ts │ ├── recallMsg.ts │ ├── setEssence.ts │ ├── getMemberProfile.ts │ ├── removeMember.ts │ ├── getMemberInfo.ts │ ├── mute.ts │ ├── getMsg.ts │ ├── setMemberPerm.ts │ ├── fs │ │ ├── renameGroupFile.ts │ │ ├── moveGroupFile.ts │ │ ├── makeDirectory.ts │ │ ├── deleteFile.ts │ │ ├── getGroupFileInfo.ts │ │ ├── getDownloadInfo.ts │ │ ├── getGroupFileList.ts │ │ └── uploadFile.ts │ ├── sendNudge.ts │ ├── setMemberInfo.ts │ ├── sendGroupMsg.ts │ ├── sendFriendMsg.ts │ ├── event │ │ ├── botInvited.ts │ │ ├── newFriend.ts │ │ └── memberJoin.ts │ ├── sendTempMsg.ts │ ├── uploadImage.ts │ ├── uploadVoice.ts │ └── listenBegin.ts ├── index.ts ├── Avatar.ts ├── Waiter.ts ├── Base.ts ├── Message.ts ├── File.ts ├── Middleware.ts ├── Event.ts └── Bot.ts ├── .eslintrc.json ├── package.json ├── tsconfig.json ├── README.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.json 2 | package-lock.json 3 | tsconfig.json 4 | src/ 5 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | - match: 2 | dependency_type: all 3 | update_type: 'semver:patch' 4 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | dts: true, 6 | target: ['esnext'], 7 | format: ['esm', 'cjs'], 8 | outDir: 'dist', 9 | sourcemap: true 10 | }) 11 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: auto-merge 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | auto-merge: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 12 | with: 13 | github-token: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /src/Error.ts: -------------------------------------------------------------------------------- 1 | /** Mirai 错误 */ 2 | export class MiraiError extends Error { 3 | /** 错误码 */ 4 | code: number 5 | /** 错误信息 */ 6 | msg: string 7 | /** 8 | * 构造Mirai 错误 9 | * @param code 错误码 10 | * @param msg 错误消息 11 | */ 12 | constructor(code: number, msg: string) { 13 | super(`[Mirai Error:${code}:${JSON.stringify(msg)}]`) 14 | ;[this.code, this.msg] = [code, msg] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/core/avatar/getGroupAvatar.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { GroupID } from '../../Base' 3 | /** 4 | * 获取群头像。 5 | * @param qq 群号。 6 | * @returns 图像Buffer数据。 7 | */ 8 | export default async (qq: GroupID): Promise => { 9 | const responseData = await axios.get( 10 | `http://p.qlogo.cn/gh/${qq}/${qq}/100`, 11 | { 12 | responseType: 'arraybuffer' 13 | } 14 | ) 15 | return Buffer.from(responseData.data) 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": { 14 | "indent": ["error", 2, { "SwitchCase": 1 }], 15 | "linebreak-style": ["error", "unix"], 16 | "quotes": ["error", "single"], 17 | "semi": ["error", "never"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/core/avatar/getUserAvatar.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { UserID } from '../../Base' 3 | /** 4 | * 获取用户头像。 5 | * @param qq 用户的QQ。 6 | * @param res 解像度。可选640或140或100。 7 | * @returns 图像Buffer数据。 8 | */ 9 | export default async (qq: UserID, res: 640 | 140 | 100): Promise => { 10 | const responseData = await axios.get( 11 | 'https://q2.qlogo.cn/headimg_dl', 12 | { 13 | params: { 14 | dst_uin: qq, 15 | spec: res 16 | }, 17 | responseType: 'arraybuffer' 18 | } 19 | ) 20 | return Buffer.from(responseData.data) 21 | } 22 | -------------------------------------------------------------------------------- /src/core/listenStop.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | /** 3 | * 停止侦听事件 4 | * @param ws 建立连接的 WebSocket 实例 5 | */ 6 | export default async (ws: WebSocket) : Promise => { 7 | // 由于在 ws open 之前关闭连接会抛异常,故应先判断此时是否正在连接中 8 | if (ws.readyState == WebSocket.CONNECTING) { 9 | // 正在连接中,注册一个 open,等待回调时关闭 10 | // 由于是一个异步过程,使用 Promise 包装以配合开发者可能存在的同步调用 11 | await new Promise(resolve => { 12 | ws.onopen = () => { 13 | // 关闭 websocket 的连接 14 | ws.close(1000) 15 | resolve() 16 | } 17 | }) 18 | } else if (ws.readyState == WebSocket.OPEN) { 19 | // 关闭 websocket 的连接 20 | ws.close(1000) 21 | } 22 | // CLOSING or CLOSED 23 | } 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Bot, EventIndex, Processor, Matcher } from './Bot' 2 | import * as Event from './Event' 3 | import * as Message from './Message' 4 | import * as Base from './Base' 5 | import * as Middleware from './Middleware' 6 | import * as File from './File' 7 | import { MiraiError } from './Error' 8 | import { Waiter } from './Waiter' 9 | import { Avatar } from './Avatar' 10 | export { 11 | Bot, 12 | EventIndex, 13 | Processor, 14 | Matcher, 15 | File, 16 | Event, 17 | Message, 18 | Base, 19 | Middleware, 20 | MiraiError, 21 | Waiter, 22 | Avatar 23 | } 24 | export default { 25 | Bot, 26 | EventIndex, 27 | File, 28 | Event, 29 | Message, 30 | Base, 31 | Middleware, 32 | MiraiError, 33 | Waiter, 34 | Avatar 35 | } 36 | -------------------------------------------------------------------------------- /src/core/release.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { UserID } from '../Base' 4 | /** 5 | * 关闭一个会话 6 | * @param option 设定。 7 | * @param option.httpUrl http接口 8 | * @param option.sessionKey sessionKey 9 | * @param option.qq qq号 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey, 14 | qq 15 | }: { 16 | httpUrl: string 17 | sessionKey: string 18 | qq: UserID 19 | }) : Promise => { 20 | // 请求 21 | const responseData = await axios.post<{ msg: string; code: number }>( 22 | new URL('/release', httpUrl).toString(), 23 | { sessionKey, qq } 24 | ) 25 | const { 26 | data: { msg: message, code } 27 | } = responseData 28 | // 抛出 mirai 的异常 29 | if (code && code != 0) throw new MiraiError(code, message) 30 | } 31 | -------------------------------------------------------------------------------- /src/core/auth.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | /** 4 | * 验证verifyKey并返回sessionKey。 5 | * @param option 设定 6 | * @param option.httpUrl http链接 7 | * @param option.verifyKey 认证key 8 | * @returns 一个sessionKey。 9 | */ 10 | export default async ({ 11 | httpUrl, 12 | verifyKey 13 | }: { 14 | httpUrl: string 15 | verifyKey: string 16 | }): Promise => { 17 | // 请求 18 | const responseData = await axios.post<{ 19 | msg: string 20 | code: number 21 | session: string 22 | }>(new URL('/verify', httpUrl).toString(), { verifyKey }) 23 | const { 24 | data: { msg: message, code, session: sessionKey } 25 | } = responseData 26 | // 抛出 mirai 的异常 27 | if (code && code != 0) throw new MiraiError(code, message) 28 | return sessionKey 29 | } 30 | -------------------------------------------------------------------------------- /src/core/getFriendList.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { User } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取好友列表 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @returns 好友列表 10 | */ 11 | export default async ({ httpUrl, sessionKey }:{ 12 | httpUrl: string 13 | sessionKey: string 14 | }) : Promise => { 15 | // 请求 16 | const responseData = await axios.get<{ msg: string; code: number; data:User[] }>(new URL('/friendList',httpUrl).toString(), { 17 | params: { sessionKey } 18 | }) 19 | const { 20 | data: { msg: message, code, data } 21 | } = responseData 22 | // 抛出 mirai 的异常 23 | if (code && code != 0) { 24 | throw new MiraiError(code, message) 25 | } 26 | return data 27 | } 28 | -------------------------------------------------------------------------------- /src/core/bind.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { UserID } from '../Base' 4 | /** 5 | * 校验 sessionKey,将一个 session 绑定到指定的 qq 上 6 | * @param option 设定 7 | * @param option.httpUrl http地址 8 | * @param option.sessionKey sessionKey 9 | * @param option.qq 要绑定的qq 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey, 14 | qq 15 | }: { 16 | httpUrl: string 17 | sessionKey: string 18 | qq: UserID 19 | }): Promise => { 20 | // 请求 21 | const responseData = await axios.post<{ 22 | msg: string 23 | code: number 24 | }>(new URL('/bind', httpUrl).toString(), { sessionKey, qq }) 25 | const { 26 | data: { msg: message, code } 27 | } = responseData 28 | // 抛出 mirai 的异常 29 | if (code && code != 0) throw new MiraiError(code, message) 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /src/core/getBotProfile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { Profile } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取Bot信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @returns Bot资料 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey 14 | }: { 15 | httpUrl: string 16 | sessionKey: string 17 | }): Promise => { 18 | // 请求 19 | const responseData = await axios.get( 20 | new URL('/botProfile', httpUrl).toString(), 21 | { 22 | params: { sessionKey } 23 | } 24 | ) 25 | const { data } = responseData 26 | // 抛出 mirai 的异常 27 | if (data.code && data.code != 0) { 28 | throw new MiraiError(data.code, data.msg ?? '') 29 | } 30 | return data 31 | } 32 | -------------------------------------------------------------------------------- /src/core/muteAll.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../Base' 4 | /** 5 | * 全员禁言 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 欲全员禁言的群号 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey, 14 | target 15 | }: { 16 | httpUrl: string 17 | sessionKey: string 18 | target: GroupID 19 | }): Promise => { 20 | // 请求 21 | const responseData = await axios.post<{ 22 | msg: string 23 | code: number 24 | }>(new URL('/muteAll', httpUrl).toString(), { sessionKey, target }) 25 | const { 26 | data: { msg: message, code } 27 | } = responseData 28 | // 抛出 mirai 的异常 29 | if (code && code != 0) { 30 | throw new MiraiError(code, message) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/core/quitGroup.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../Base' 4 | /** 5 | * 移除群聊 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 欲移除群号 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey, 14 | target 15 | }: { 16 | httpUrl: string 17 | sessionKey: string 18 | target: GroupID 19 | }) => { 20 | // 请求 21 | const responseData = await axios.post<{ 22 | msg: string 23 | code: number 24 | }>(new URL('/quit', httpUrl).toString(), { 25 | sessionKey, 26 | target 27 | }) 28 | const { 29 | data: { msg: message, code } 30 | } = responseData 31 | // 抛出 mirai 的异常 32 | if (code && code != 0) { 33 | throw new MiraiError(code, message) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/unmuteAll.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../Base' 4 | /** 5 | * 解除全员禁言 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 欲解除全员禁言的群号 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey, 14 | target 15 | }: { 16 | httpUrl: string 17 | sessionKey: string 18 | target: GroupID 19 | }): Promise => { 20 | // 请求 21 | const responseData = await axios.post<{ 22 | msg: string 23 | code: number 24 | }>(new URL('/unmuteAll', httpUrl).toString(), { sessionKey, target }) 25 | const { 26 | data: { msg: message, code } 27 | } = responseData 28 | // 抛出 mirai 的异常 29 | if (code && code != 0) { 30 | throw new MiraiError(code, message) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/core/getGroupList.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { Group } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取群列表 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @returns 群列表 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey 14 | }: { 15 | httpUrl: string 16 | sessionKey: string 17 | }): Promise => { 18 | // 请求 19 | const responseData = await axios.get<{ 20 | msg: string 21 | code: number 22 | data: Group[] 23 | }>(new URL('/groupList', httpUrl).toString(), { 24 | params: { sessionKey } 25 | }) 26 | const { 27 | data: { msg: message, code, data } 28 | } = responseData 29 | // 抛出 mirai 的异常 30 | if (code && code != 0) { 31 | throw new MiraiError(code, message) 32 | } 33 | return data 34 | } 35 | -------------------------------------------------------------------------------- /src/core/removeFriend.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { UserID } from '../Base' 4 | /** 5 | * 移除好友 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 欲移除好友qq号 10 | */ 11 | export default async ({ 12 | httpUrl, 13 | sessionKey, 14 | target 15 | }: { 16 | httpUrl: string 17 | sessionKey: string 18 | target: UserID 19 | }) => { 20 | // 请求 21 | const responseData = await axios.post<{ 22 | msg: string 23 | code: number 24 | }>(new URL('/deleteFriend', httpUrl).toString(), { 25 | sessionKey, 26 | target 27 | }) 28 | const { 29 | data: { msg: message, code } 30 | } = responseData 31 | // 抛出 mirai 的异常 32 | if (code && code != 0) { 33 | throw new MiraiError(code, message) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Avatar.ts: -------------------------------------------------------------------------------- 1 | import _getUserAvatar from './core/avatar/getUserAvatar' 2 | import _getGroupAvatar from './core/avatar/getGroupAvatar' 3 | import { GroupID, UserID } from './Base' 4 | /** 5 | * 获得用户头像。 6 | * @param type 要获得的类型。 7 | * @param qq 用户的qq。 8 | * @param res 解像度。可以为640或140或100。 9 | * @returns 图像Buffer数据。 10 | */ 11 | export async function Avatar( 12 | type: 'user', 13 | qq: UserID, 14 | res: 640 | 140 | 100 15 | ): Promise 16 | /** 17 | * 获得群头像。 18 | * @param type 要获得的类型。 19 | * @param qq 群号。 20 | * @returns 图像Buffer数据。 21 | */ 22 | export async function Avatar(type: 'group', qq: GroupID): Promise 23 | export async function Avatar( 24 | type: 'user' | 'group', 25 | qq: GroupID | UserID, 26 | arg?: 640 | 140 | 100 27 | ): Promise { 28 | return await (type == 'user' 29 | ? _getUserAvatar(qq, arg ?? 640) 30 | : _getGroupAvatar(qq)) 31 | } 32 | -------------------------------------------------------------------------------- /src/core/getUserProfile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { Profile, UserID } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取用户信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 用户qq 10 | * @returns 用户资料 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target 16 | }: { 17 | httpUrl: string 18 | sessionKey: string 19 | target: UserID 20 | }): Promise => { 21 | // 请求 22 | const responseData = await axios.get( 23 | new URL('/userProfile', httpUrl).toString(), 24 | { 25 | params: { sessionKey, target } 26 | } 27 | ) 28 | const { data } = responseData 29 | // 抛出 mirai 的异常 30 | if (data.code && data.code != 0) { 31 | throw new MiraiError(data.code, data.msg ?? '') 32 | } 33 | return data 34 | } 35 | -------------------------------------------------------------------------------- /src/core/getFriendProfile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { Profile, UserID } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取好友信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 好友qq 10 | * @returns 好友资料 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target 16 | }: { 17 | httpUrl: string 18 | sessionKey: string 19 | target: UserID 20 | }): Promise => { 21 | // 请求 22 | const responseData = await axios.get( 23 | new URL('/friendProfile', httpUrl).toString(), 24 | { 25 | params: { sessionKey, target } 26 | } 27 | ) 28 | const { data } = responseData 29 | // 抛出 mirai 的异常 30 | if (data.code && data.code != 0) { 31 | throw new MiraiError(data.code, data.msg ?? '') 32 | } 33 | return data 34 | } 35 | -------------------------------------------------------------------------------- /src/core/unmute.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../Base' 4 | /** 5 | * 解除禁言群成员 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 欲解除禁言成员所在群号 10 | * @param option.memberId 欲解除禁言成员 qq 号 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target, 16 | memberId 17 | }: { 18 | httpUrl: string 19 | sessionKey: string 20 | target: GroupID 21 | memberId: UserID 22 | }) => { 23 | // 请求 24 | const responseData = await axios.post<{ 25 | msg: string 26 | code: number 27 | }>(new URL('/unmute', httpUrl).toString(), { sessionKey, target, memberId }) 28 | const { 29 | data: { msg: message, code } 30 | } = responseData 31 | // 抛出 mirai 的异常 32 | if (code && code != 0) { 33 | throw new MiraiError(code, message) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/anno/deleteAnno.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../../Base' 4 | /** 5 | * 删除群公告 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.id 群号 10 | * @param option.fid 公告 id 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | id, 16 | fid 17 | }: { 18 | httpUrl: string 19 | sessionKey: string 20 | id: GroupID 21 | fid: string 22 | }) : Promise => { 23 | // 请求 24 | const responseData = await axios.post<{ 25 | msg: string 26 | code: number 27 | }>(new URL('/anno/delete', httpUrl).toString(), { 28 | sessionKey, 29 | id, 30 | fid 31 | }) 32 | const { 33 | data: { msg: message, code } 34 | } = responseData 35 | // 抛出 mirai 的异常 36 | if (code && code != 0) { 37 | throw new MiraiError(code, message) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/core/getGroupConfig.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, GroupInfo } from '../Base' 4 | /** 5 | * 获取群信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 群号 10 | * @returns 群信息 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target 16 | }: { 17 | httpUrl: string 18 | sessionKey: string 19 | target: GroupID 20 | }): Promise => { 21 | // 请求 22 | const responseData = await axios.get< 23 | GroupInfo & { 24 | code?: number 25 | msg?: string 26 | } 27 | >(new URL('/groupConfig', httpUrl).toString(), { 28 | params: { sessionKey, target } 29 | }) 30 | const { data } = responseData 31 | // 抛出 mirai 的异常 32 | if (data.code && data.code != 0) { 33 | throw new MiraiError(data.code, data.msg ?? '') 34 | } 35 | return data 36 | } 37 | -------------------------------------------------------------------------------- /src/core/getMemberList.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { GroupID, Member } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取群成员列表 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 目标群组 10 | * @returns 成员列表 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target 16 | }: { 17 | httpUrl: string 18 | sessionKey: string 19 | target: GroupID 20 | }): Promise => { 21 | // 请求 22 | const responseData = await axios.get<{ 23 | msg: string 24 | code: number 25 | data: Member[] 26 | }>(new URL('/memberList', httpUrl).toString(), { 27 | params: { sessionKey,target } 28 | }) 29 | const { 30 | data: { msg: message, code, data } 31 | } = responseData 32 | // 抛出 mirai 的异常 33 | if (code && code != 0) { 34 | throw new MiraiError(code, message) 35 | } 36 | return data 37 | } 38 | -------------------------------------------------------------------------------- /src/core/setGroupConfig.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, GroupInfo } from '../Base' 4 | /** 5 | * 设定群信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 群号 10 | * @param option.info 群信息 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target, 16 | info 17 | }: { 18 | httpUrl: string 19 | sessionKey: string 20 | target: GroupID 21 | info: GroupInfo 22 | }): Promise => { 23 | // 请求 24 | const responseData = await axios.post<{ 25 | code: number 26 | msg: string 27 | }>(new URL('/groupConfig', httpUrl).toString(), { 28 | sessionKey, 29 | target, 30 | config: info 31 | }) 32 | const { 33 | data: { msg: message, code } 34 | } = responseData 35 | // 抛出 mirai 的异常 36 | if (code && code != 0) { 37 | throw new MiraiError(code, message) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/core/recallMsg.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../Base' 4 | /** 5 | * 撤回由 messageId 确定的消息 6 | * @param option 选项。 7 | * @param option.httpUrl mirai-api-http server 的主机地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 目标群号/好友QQ 10 | * @param option.messageId 欲撤回消息的 messageId 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target, 16 | messageId 17 | }: { 18 | httpUrl: string 19 | sessionKey: string 20 | target: UserID | GroupID, 21 | messageId: number 22 | }): Promise => { 23 | // 请求 24 | const responseData = await axios.post<{ 25 | code: number 26 | msg: string 27 | }>(new URL('/recall', httpUrl).toString(), { sessionKey, target, messageId }) 28 | const { 29 | data: { code, msg: message } 30 | } = responseData 31 | // 抛出 mirai 的异常 32 | if (code && code != 0) { 33 | throw new MiraiError(code, message) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/setEssence.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../Base' 4 | /** 5 | * 将由 messageId 确定的消息设置为本群精华消息 6 | * @param option 选项。 7 | * @param option.httpUrl mirai-api-http server 的主机地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 群号 10 | * @param option.messageId 欲设置群精华消息的 messageId 11 | */ 12 | export default async ({ 13 | httpUrl, 14 | sessionKey, 15 | target, 16 | messageId 17 | }: { 18 | httpUrl: string 19 | sessionKey: string 20 | target: GroupID 21 | messageId: number 22 | }): Promise => { 23 | // 请求 24 | const responseData = await axios.post<{ 25 | code: number 26 | msg: string 27 | }>(new URL('/setEssence', httpUrl).toString(), { 28 | sessionKey, 29 | target, 30 | messageId 31 | }) 32 | const { 33 | data: { code, msg: message } 34 | } = responseData 35 | // 抛出 mirai 的异常 36 | if (code && code != 0) { 37 | throw new MiraiError(code, message) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/core/getMemberProfile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { GroupID, Profile, UserID } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取成员信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target qq群号 10 | * @param option.memberId 成员qq 11 | * @returns 成员资料 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | memberId 18 | }: { 19 | httpUrl: string 20 | sessionKey: string 21 | target: GroupID 22 | memberId: UserID 23 | }): Promise => { 24 | // 请求 25 | const responseData = await axios.get( 26 | new URL('/memberProfile', httpUrl).toString(), 27 | { 28 | params: { sessionKey, target, memberId } 29 | } 30 | ) 31 | const { data } = responseData 32 | // 抛出 mirai 的异常 33 | if (data.code && data.code != 0) { 34 | throw new MiraiError(data.code, data.msg ?? '') 35 | } 36 | return data 37 | } 38 | -------------------------------------------------------------------------------- /src/core/removeMember.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../Base' 4 | /** 5 | * 移除群成员 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 欲移除成员所在群号 10 | * @param option.memberId 欲移除成员 qq 号 11 | * @param option.msg 移除信息 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | memberId, 18 | msg 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | target: GroupID 23 | memberId: UserID 24 | msg: string 25 | }) => { 26 | // 请求 27 | const responseData = await axios.post<{ 28 | msg: string 29 | code: number 30 | }>(new URL('/kick', httpUrl).toString(), { 31 | sessionKey, 32 | target, 33 | memberId, 34 | msg 35 | }) 36 | const { 37 | data: { msg: message, code } 38 | } = responseData 39 | // 抛出 mirai 的异常 40 | if (code && code != 0) { 41 | throw new MiraiError(code, message) 42 | } 43 | } -------------------------------------------------------------------------------- /src/core/getMemberInfo.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { GroupID, Member, UserID } from '../Base' 3 | import axios from 'axios' 4 | /** 5 | * 获取群成员信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 群成员所在群号 10 | * @param option.memberId 群成员的 qq 号 11 | * @returns 群成员信息 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | memberId 18 | }: { 19 | httpUrl: string 20 | sessionKey: string 21 | target: GroupID 22 | memberId: UserID 23 | }): Promise => { 24 | // 请求 25 | const responseData = await axios.get( 26 | new URL('/memberInfo', httpUrl).toString(), 27 | { 28 | params: { sessionKey, target, memberId } 29 | } 30 | ) 31 | const { data } = responseData 32 | // 抛出 mirai 的异常 33 | if (data.code && data.code != 0) { 34 | throw new MiraiError(data.code, data.msg ?? '') 35 | } 36 | return data as Member 37 | } 38 | -------------------------------------------------------------------------------- /src/core/mute.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../Base' 4 | /** 5 | * 禁言群成员 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 欲禁言成员所在群号 10 | * @param option.memberId 欲禁言成员 qq 号 11 | * @param option.time 禁言时长,单位: s (秒) 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | memberId, 18 | time 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | target: GroupID 23 | memberId: UserID 24 | time: number 25 | }): Promise => { 26 | // 请求 27 | const responseData = await axios.post<{ 28 | msg: string 29 | code: number 30 | }>(new URL('/mute', httpUrl).toString(), { 31 | sessionKey, 32 | target, 33 | memberId, 34 | time 35 | }) 36 | const { 37 | data: { msg: message, code } 38 | } = responseData 39 | // 抛出 mirai 的异常 40 | if (code && code != 0) { 41 | throw new MiraiError(code, message) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/core/anno/publishAnno.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../../Base' 4 | /** 5 | * 发布群公告 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 群号 10 | * @param option.content 公告内容 11 | * @param option.pinned 是否置顶 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | content, 18 | pinned = false 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | target: GroupID 23 | content: string 24 | pinned?: boolean 25 | }): Promise => { 26 | // 请求 27 | const responseData = await axios.post<{ 28 | msg: string 29 | code: number 30 | }>(new URL('/anno/publish', httpUrl).toString(), { 31 | sessionKey, 32 | target, 33 | content, 34 | pinned 35 | }) 36 | const { 37 | data: { msg: message, code } 38 | } = responseData 39 | // 抛出 mirai 的异常 40 | if (code && code != 0) { 41 | throw new MiraiError(code, message) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/core/getMsg.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { Message } from '../Event' 4 | import { GroupID, UserID } from '../Base' 5 | /** 6 | * 由messageId取信息 7 | * @param option 选项 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.target 目标群号/好友 11 | * @param option.messageId 消息id 12 | * @returns 信息 13 | */ 14 | export default async ({ 15 | httpUrl, 16 | sessionKey, 17 | messageId, 18 | target 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | messageId: number 23 | target: GroupID | UserID 24 | }): Promise => { 25 | // 请求 26 | const responseData = await axios.get<{ 27 | code: number 28 | msg: string 29 | data: Message 30 | }>(new URL('/messageFromId', httpUrl).toString(), { 31 | params: { sessionKey, messageId, target } 32 | }) 33 | const { 34 | data: { code, msg: message, data } 35 | } = responseData 36 | // 抛出 mirai 的异常 37 | if (code && code != 0) { 38 | throw new MiraiError(code, message) 39 | } 40 | return data 41 | } 42 | -------------------------------------------------------------------------------- /src/core/setMemberPerm.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../Base' 4 | /** 5 | * 设置群成员权限 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 群成员所在群号 10 | * @param option.memberId 群成员的 qq 号 11 | * @param option.assign 是否设置为管理员 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | memberId, 18 | assign 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | target: GroupID 23 | memberId: UserID 24 | assign: boolean 25 | }): Promise => { 26 | // 请求 27 | const responseData = await axios.post<{ code: number; msg: string }>( 28 | new URL('/memberAdmin', httpUrl).toString(), 29 | { 30 | sessionKey, 31 | target, 32 | memberId, 33 | assign 34 | } 35 | ) 36 | const { 37 | data: { msg: message, code } 38 | } = responseData 39 | // 抛出 mirai 的异常 40 | if (code && code != 0) { 41 | throw new MiraiError(code, message) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/core/fs/renameGroupFile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../../Base' 4 | /** 5 | * 重命名群文件 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.path 文件路径。 10 | * @param option.renameTo 重命名到指定文件名 11 | * @param option.group 群号 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | path, 17 | group, 18 | renameTo 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | path: string 23 | group: GroupID 24 | renameTo: string 25 | }): Promise => { 26 | // 请求 27 | const responseData = await axios.post<{ msg: string; code: number }>( 28 | new URL('/file/rename', httpUrl).toString(), 29 | { 30 | sessionKey, 31 | path, 32 | target: group, 33 | group, 34 | renameTo 35 | } 36 | ) 37 | const { 38 | data: { msg: message, code } 39 | } = responseData 40 | // 抛出 mirai 的异常 41 | if (code && code != 0) { 42 | throw new MiraiError(code, message) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/sendNudge.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../Base' 4 | /** 5 | * 发送戳一戳消息 6 | * @param option 设定。 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 戳一戳的目标 10 | * @param option.subject 戳一戳的上下文,群或好友 11 | * @param option.kind 上下文类型, 可选值 Friend, Group 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | subject, 18 | kind 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | target: UserID 23 | subject: UserID | GroupID 24 | kind: 'Friend' | 'Group' 25 | }) : Promise => { 26 | // 请求 27 | const responseData = await axios.post<{ msg: string; code: number }>( 28 | new URL('/sendNudge', httpUrl).toString(), 29 | { 30 | sessionKey, 31 | target, 32 | subject, 33 | kind 34 | } 35 | ) 36 | const { 37 | data: { msg: message, code } 38 | } = responseData 39 | // 抛出 mirai 的异常 40 | if (code && code != 0) { 41 | throw new MiraiError(code, message) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/core/setMemberInfo.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../Base' 4 | /** 5 | * 设置群成员信息 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.target 群成员所在群号 10 | * @param option.memberId 群成员的 qq 号 11 | * @param option.info 信息 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | target, 17 | memberId, 18 | info 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | target: GroupID 23 | memberId: UserID 24 | info: { 25 | name?: string 26 | specialTitle?: string 27 | } 28 | }): Promise => { 29 | // 请求 30 | const responseData = await axios.post<{ code: number; msg: string }>( 31 | new URL('/memberInfo', httpUrl).toString(), 32 | { 33 | sessionKey, 34 | target, 35 | memberId, 36 | info 37 | } 38 | ) 39 | const { 40 | data: { msg: message, code } 41 | } = responseData 42 | // 抛出 mirai 的异常 43 | if (code && code != 0) { 44 | throw new MiraiError(code, message) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/core/fs/moveGroupFile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../../Base' 4 | /** 5 | * 移动群文件 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.path 文件路径。 10 | * @param option.moveToPath 移动到指定path 11 | * @param option.moveTo 移动到指定id 12 | * @param option.group 群号 13 | */ 14 | export default async ({ 15 | httpUrl, 16 | sessionKey, 17 | path, 18 | group, 19 | moveToPath 20 | }: { 21 | httpUrl: string 22 | sessionKey: string 23 | path: string 24 | group: GroupID 25 | moveToPath: string 26 | }): Promise => { 27 | // 请求 28 | const responseData = await axios.post<{ msg: string; code: number }>( 29 | new URL('/file/move', httpUrl).toString(), 30 | { 31 | sessionKey, 32 | path, 33 | target: group, 34 | group, 35 | moveToPath 36 | } 37 | ) 38 | const { 39 | data: { msg: message, code } 40 | } = responseData 41 | // 抛出 mirai 的异常 42 | if (code && code != 0) { 43 | throw new MiraiError(code, message) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/anno/getAnnoList.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { Announcement, GroupID } from '../../Base' 4 | /** 5 | * 获取群公告 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.id 群号 10 | * @param option.offset 分页 11 | * @param option.size 分页, 默认 10 12 | * @returns 公告列表 13 | */ 14 | export default async ({ 15 | httpUrl, 16 | sessionKey, 17 | id, 18 | offset, 19 | size = 10 20 | }: { 21 | httpUrl: string 22 | sessionKey: string 23 | id: GroupID 24 | offset: number 25 | size?: number 26 | }): Promise => { 27 | // 请求 28 | const responseData = await axios.get<{ 29 | msg: string 30 | code: number 31 | data: Announcement[] 32 | }>(new URL('/anno/list', httpUrl).toString(), { 33 | params: { 34 | sessionKey, 35 | id, 36 | offset, 37 | size 38 | } 39 | }) 40 | const { 41 | data: { msg: message, code, data } 42 | } = responseData 43 | // 抛出 mirai 的异常 44 | if (code && code != 0) { 45 | throw new MiraiError(code, message) 46 | } 47 | return data 48 | } 49 | -------------------------------------------------------------------------------- /src/core/fs/makeDirectory.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../../Base' 4 | import { FileDetail } from '../../File' 5 | /** 6 | * 群创建文件夹 7 | * @param option 选项 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.path 父文件夹路径。 11 | * @param option.directoryName 新文件夹名称 12 | * @param option.group 群号 13 | */ 14 | export default async ({ 15 | httpUrl, 16 | sessionKey, 17 | path, 18 | group, 19 | directoryName 20 | }: { 21 | httpUrl: string 22 | sessionKey: string 23 | path: string 24 | group: GroupID 25 | directoryName: string 26 | }): Promise => { 27 | // 请求 28 | const responseData = await axios.post<{ 29 | msg: string 30 | code: number 31 | data: FileDetail 32 | }>(new URL('/file/mkdir', httpUrl).toString(), { 33 | sessionKey, 34 | path, 35 | target: group, 36 | group, 37 | directoryName 38 | }) 39 | const { 40 | data: { msg: message, code, data } 41 | } = responseData 42 | // 抛出 mirai 的异常 43 | if (code && code != 0) { 44 | throw new MiraiError(code, message) 45 | } 46 | return data 47 | } 48 | -------------------------------------------------------------------------------- /src/core/fs/deleteFile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../../Base' 4 | /** 5 | * 删除文件 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.path 文件夹路径。 10 | * @param option.type 'friend' 或 'group' 11 | * @param option.target qq号 12 | */ 13 | export default async ({ 14 | httpUrl, 15 | sessionKey, 16 | path, 17 | target, 18 | type 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | path: string 23 | target: UserID | GroupID 24 | type: 'friend' | 'group' 25 | }): Promise => { 26 | // TODO: 等到mirai-api-http修正接口后修改调用方式 27 | // 请求 28 | const responseData = await axios.post<{ msg: string; code: number }>( 29 | new URL('/file/delete', httpUrl).toString(), 30 | { 31 | sessionKey, 32 | path, 33 | target, 34 | group: type == 'group' ? target : undefined, 35 | qq: type == 'friend' ? target : undefined 36 | } 37 | ) 38 | const { 39 | data: { msg: message, code } 40 | } = responseData 41 | // 抛出 mirai 的异常 42 | if (code && code != 0) { 43 | throw new MiraiError(code, message) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/sendGroupMsg.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../Base' 4 | import { MessageBase } from '../Message' 5 | /** 6 | * 向 qq 群发送消息 7 | * @param option 选项。 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.target 目标群号 11 | * @param option.quote 消息引用,使用发送时返回的 messageId 12 | * @param option.messageChain 消息链,MessageType 数组 13 | * @returns messageId 14 | */ 15 | export default async ({ 16 | httpUrl, 17 | sessionKey, 18 | target, 19 | quote, 20 | messageChain 21 | }: { 22 | httpUrl: string 23 | sessionKey: string 24 | target: GroupID 25 | quote?: number 26 | messageChain: MessageBase[] 27 | }): Promise => { 28 | // 请求 29 | const responseData = await axios.post<{ 30 | msg: string 31 | code: number 32 | messageId: number 33 | }>(new URL('/sendGroupMessage', httpUrl).toString(), { 34 | sessionKey, 35 | target, 36 | quote, 37 | messageChain 38 | }) 39 | const { 40 | data: { msg: message, code, messageId } 41 | } = responseData 42 | // 抛出 mirai 的异常 43 | if (code && code != 0) { 44 | throw new MiraiError(code, message) 45 | } 46 | return messageId 47 | } 48 | -------------------------------------------------------------------------------- /src/core/sendFriendMsg.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { UserID } from '../Base' 4 | import { MessageBase } from '../Message' 5 | /** 6 | * 向 qq 好友发送消息 7 | * @param option 设定 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.target 目标好友 qq 号 11 | * @param option.quote 消息引用,使用发送时返回的 messageId 12 | * @param option.messageChain 消息链,MessageType 数组 13 | * @returns messageId 14 | */ 15 | export default async ({ 16 | httpUrl, 17 | sessionKey, 18 | target, 19 | quote, 20 | messageChain 21 | }: { 22 | httpUrl: string 23 | sessionKey: string 24 | target: UserID 25 | quote?: number 26 | messageChain: MessageBase[] 27 | }): Promise => { 28 | // 请求 29 | const responseData = await axios.post<{ 30 | msg: string 31 | code: number 32 | messageId: number 33 | }>(new URL('/sendFriendMessage', httpUrl).toString(), { 34 | sessionKey, 35 | target, 36 | quote, 37 | messageChain 38 | }) 39 | const { 40 | data: { msg: message, code, messageId } 41 | } = responseData 42 | // 抛出 mirai 的异常 43 | if (code && code != 0) { 44 | throw new MiraiError(code, message) 45 | } 46 | return messageId 47 | } 48 | -------------------------------------------------------------------------------- /src/core/fs/getGroupFileInfo.ts: -------------------------------------------------------------------------------- 1 | import { GroupID } from '../../Base' 2 | import { FileDetail } from '../../File' 3 | import { MiraiError } from '../../Error' 4 | import axios from 'axios' 5 | /** 6 | * 获取群文件信息 7 | * @param option 选项 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.id 文件夹id, 空串为根目录 11 | * @param option.path 文件路径。 12 | * @param option.group 群号 13 | * @returns 群文件信息 14 | */ 15 | export default async ({ 16 | httpUrl, 17 | sessionKey, 18 | id, 19 | path, 20 | group 21 | }: { 22 | httpUrl: string 23 | sessionKey: string 24 | id?: string 25 | path: null | string 26 | group: GroupID 27 | }): Promise => { 28 | // 请求 29 | const responseData = await axios.get<{ 30 | code: number 31 | msg: string 32 | data: FileDetail 33 | }>(new URL('/file/info', httpUrl).toString(), { 34 | params: { 35 | sessionKey, 36 | id, 37 | path, 38 | target: group, 39 | group, 40 | withDownloadInfo: false 41 | } 42 | }) 43 | const { 44 | data: { msg: message, code, data } 45 | } = responseData 46 | // 抛出 mirai 的异常 47 | if (code && code != 0) { 48 | throw new MiraiError(code, message) 49 | } 50 | return data 51 | } 52 | -------------------------------------------------------------------------------- /src/core/fs/getDownloadInfo.ts: -------------------------------------------------------------------------------- 1 | import { GroupID } from '../../Base' 2 | import { DownloadInfo } from '../../File' 3 | import { MiraiError } from '../../Error' 4 | import axios from 'axios' 5 | /** 6 | * 获取下载信息 7 | * @param option 选项 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.path 文件夹路径,允许重名,尽可能使用id。 11 | * @param option.group 群号 12 | * @returns 群文件下载信息 13 | */ 14 | export default async ({ 15 | httpUrl, 16 | sessionKey, 17 | path, 18 | group 19 | }: { 20 | httpUrl: string 21 | sessionKey: string 22 | path: string 23 | group: GroupID 24 | }): Promise => { 25 | // 请求 26 | const responseData = await axios.get<{ 27 | code: number 28 | msg: string 29 | data: { 30 | downloadInfo: DownloadInfo 31 | } 32 | }>(new URL('/file/info', httpUrl).toString(), { 33 | params: { 34 | sessionKey, 35 | path, 36 | target: group, 37 | group, 38 | withDownloadInfo: true 39 | } 40 | }) 41 | const { 42 | data: { 43 | msg: message, 44 | code, 45 | data: { downloadInfo } 46 | } 47 | } = responseData 48 | // 抛出 mirai 的异常 49 | if (code && code != 0) { 50 | throw new MiraiError(code, message) 51 | } 52 | return downloadInfo 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/typescript.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Typescript CI 5 | 6 | on: 7 | push: 8 | branches: ['master'] 9 | pull_request: 10 | branches: ['master'] 11 | 12 | jobs: 13 | build: 14 | environment: ci 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - name: Checkout branch 24 | uses: actions/checkout@v3 25 | - name: Setup Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | - name: Install dependencies 31 | run: npm ci 32 | - name: Build mirai-foxes 33 | run: npm run build --if-present 34 | - name: Publish package 35 | uses: JS-DevTools/npm-publish@v2 36 | env: 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | if: env.NPM_TOKEN != '' 39 | with: 40 | token: ${{ env.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirai-foxes", 3 | "version": "1.5.16", 4 | "description": "一个根据 Mirai-js 代码重写,运行在 Node.js 下的 Typescript QQ 机器人开发框架", 5 | "main": "./dist/index.js", 6 | "exports": { 7 | ".": { 8 | "import": "./dist/index.mjs", 9 | "require": "./dist/index.js" 10 | } 11 | }, 12 | "scripts": { 13 | "lint": "eslint ./src", 14 | "lint:type": "tsc --noEmit -p ./tsconfig.json", 15 | "fix": "eslint ./src --fix", 16 | "build": "tsup" 17 | }, 18 | "publishConfig": { 19 | "access": "public", 20 | "registry": "https://registry.npmjs.com" 21 | }, 22 | "types": "./dist/index.d.ts", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/FurDevsCN/mirai-foxes.git" 26 | }, 27 | "author": "FurDevsCN", 28 | "license": "AGPL-3.0", 29 | "bugs": { 30 | "url": "https://github.com/FurDevsCN/mirai-foxes/issues" 31 | }, 32 | "homepage": "https://github.com/FurDevsCN/mirai-foxes#readme", 33 | "devDependencies": { 34 | "@types/node": "^18.11.18", 35 | "@types/ws": "^8.5.3", 36 | "@typescript-eslint/eslint-plugin": "^5.32.0", 37 | "@typescript-eslint/parser": "^5.32.0", 38 | "eslint": "^8.21.0", 39 | "typescript": "^5.1.6", 40 | "tsup": "^6.5.0" 41 | }, 42 | "dependencies": { 43 | "axios": "^1.2.1", 44 | "form-data": "^4.0.0", 45 | "ws": "^8.8.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/core/event/botInvited.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { BotInvitedJoinGroupRequestEvent } from '../../Event' 4 | const Operate = { 5 | accept: 0, 6 | refuse: 1 7 | } 8 | /** 9 | * 处理Bot被邀请入群事件 10 | * @param option 选项 11 | * @param option.httpUrl mirai-api-http server 的地址 12 | * @param option.sessionKey 会话标识 13 | * @param option.event 事件 14 | * @param option.option 选项 15 | * @param option.option.action 动作 16 | * @param option.option.message 消息 17 | */ 18 | export default async ({ 19 | httpUrl, 20 | sessionKey, 21 | event, 22 | option 23 | }: { 24 | httpUrl: string 25 | sessionKey: string 26 | event: BotInvitedJoinGroupRequestEvent 27 | option: { 28 | action: 'accept' | 'refuse' 29 | message?: string 30 | } 31 | }): Promise => { 32 | // 请求 33 | const responseData = await axios.post<{ 34 | code: number 35 | msg: string 36 | }>(new URL('/resp/botInvitedJoinGroupRequestEvent', httpUrl).toString(), { 37 | sessionKey, 38 | eventId: event.eventId, 39 | fromId: event.fromId, 40 | groupId: event.groupId, 41 | operate: Operate[option.action], 42 | message: option.message ??'' 43 | }) 44 | const { 45 | data: { msg: message, code } 46 | } = responseData 47 | // 抛出 mirai 的异常 48 | if (code && code != 0) { 49 | throw new MiraiError(code, message) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/core/event/newFriend.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { NewFriendRequestEvent } from '../../Event' 4 | const Operate = { 5 | accept: 0, 6 | refuse: 1, 7 | refusedie: 2, 8 | } 9 | /** 10 | * 处理添加好友事件 11 | * @param option 选项 12 | * @param option.httpUrl mirai-api-http server 的地址 13 | * @param option.sessionKey 会话标识 14 | * @param option.event 事件 15 | * @param option.option 选项 16 | * @param option.option.action 动作 17 | * @param option.option.message 消息 18 | */ 19 | export default async ({ 20 | httpUrl, 21 | sessionKey, 22 | event, 23 | option 24 | }: { 25 | httpUrl: string 26 | sessionKey: string 27 | event: NewFriendRequestEvent 28 | option: { 29 | action: 'accept' | 'refuse' | 'refusedie' 30 | message?: string 31 | } 32 | }): Promise => { 33 | // 请求 34 | const responseData = await axios.post<{ 35 | code: number 36 | msg: string 37 | }>(new URL('/resp/newFriendRequestEvent', httpUrl).toString(), { 38 | sessionKey, 39 | eventId: event.eventId, 40 | fromId: event.fromId, 41 | groupId: event.groupId, 42 | operate: Operate[option.action], 43 | message: option.message ?? '' 44 | }) 45 | const { 46 | data: { msg: message, code } 47 | } = responseData 48 | // 抛出 mirai 的异常 49 | if (code && code != 0) { 50 | throw new MiraiError(code, message) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/core/fs/getGroupFileList.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID } from '../../Base' 4 | import { FileDetail } from '../../File' 5 | /** 6 | * 获取群文件列表 7 | * @param option 选项 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.path 文件夹路径,允许重名,尽可能使用id。 11 | * @param option.group 群号 12 | * @param option.offset 分页偏移 13 | * @param option.size 分页大小 14 | * @returns 群文件列表 15 | */ 16 | export default async ({ 17 | httpUrl, 18 | sessionKey, 19 | path, 20 | group, 21 | offset, 22 | size 23 | }: { 24 | httpUrl: string 25 | sessionKey: string 26 | path: string 27 | group: GroupID 28 | offset: number 29 | size: number 30 | }): Promise => { 31 | // 请求 32 | const responseData = await axios.get<{ 33 | code: number 34 | msg: string 35 | data: FileDetail[] 36 | }>(new URL('/file/list', httpUrl).toString(), { 37 | params: { 38 | sessionKey, 39 | path, 40 | target: group, 41 | group, 42 | withDownloadInfo: false, 43 | offset, 44 | size 45 | } 46 | }) 47 | const { 48 | data: { msg: message, code, data } 49 | } = responseData 50 | // 抛出 mirai 的异常 51 | if (code && code != 0) { 52 | throw new MiraiError(code, message) 53 | } 54 | return data 55 | } 56 | -------------------------------------------------------------------------------- /src/core/sendTempMsg.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import { MessageBase } from '../Message' 4 | import { GroupID, UserID } from '../Base' 5 | /** 6 | * 向临时对象发送消息 7 | * @param option 设定 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.qq 目标 qq 号 11 | * @param option.group 目标群号 12 | * @param option.quote 消息引用,使用发送时返回的 messageId 13 | * @param option.messageChain 消息链,MessageType 数组 14 | * @returns messageId 15 | */ 16 | export default async ({ 17 | httpUrl, 18 | sessionKey, 19 | qq, 20 | group, 21 | quote, 22 | messageChain 23 | }: { 24 | httpUrl: string 25 | sessionKey: string 26 | qq?: UserID 27 | group?: GroupID 28 | quote?: number 29 | messageChain: MessageBase[] 30 | }): Promise => { 31 | if (!qq && !group) { 32 | throw new Error('sendTempMessage 缺少必要的 qq|group 参数') 33 | } 34 | // 请求 35 | const responseData = await axios.post<{ 36 | msg: string 37 | code: number 38 | messageId: number 39 | }>(new URL('/sendTempMessage', httpUrl).toString(), { 40 | sessionKey, 41 | qq, 42 | group, 43 | quote, 44 | messageChain 45 | }) 46 | const { 47 | data: { msg: message, code, messageId } 48 | } = responseData 49 | // 抛出 mirai 的异常 50 | if (code && code != 0) { 51 | throw new MiraiError(code, message) 52 | } 53 | return messageId 54 | } 55 | -------------------------------------------------------------------------------- /src/core/event/memberJoin.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { MemberJoinRequestEvent } from '../../Event' 4 | const Operate = { 5 | accept: 0, 6 | refuse: 1, 7 | ignore: 2, 8 | refusedie: 3, 9 | ignoredie: 4 10 | } 11 | /** 12 | * 处理成员入群事件 13 | * @param option 选项 14 | * @param option.httpUrl mirai-api-http server 的地址 15 | * @param option.sessionKey 会话标识 16 | * @param option.event 事件 17 | * @param option.option 选项 18 | * @param option.option.action 动作 19 | * @param option.option.message 消息 20 | */ 21 | export default async ({ 22 | httpUrl, 23 | sessionKey, 24 | event, 25 | option 26 | }: { 27 | httpUrl: string 28 | sessionKey: string 29 | event: MemberJoinRequestEvent 30 | option: { 31 | action: 'accept' | 'refuse' | 'ignore' | 'refusedie' | 'ignoredie' 32 | message?: string 33 | } 34 | }): Promise => { 35 | // 请求 36 | const responseData = await axios.post<{ 37 | code: number 38 | msg: string 39 | }>(new URL('/resp/memberJoinRequestEvent', httpUrl).toString(), { 40 | sessionKey, 41 | eventId: event.eventId, 42 | fromId: event.fromId, 43 | groupId: event.groupId, 44 | operate: Operate[option.action], 45 | message: option.message ?? '' 46 | }) 47 | const { 48 | data: { msg: message, code } 49 | } = responseData 50 | // 抛出 mirai 的异常 51 | if (code && code != 0) { 52 | throw new MiraiError(code, message) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/core/uploadImage.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import axios from 'axios' 3 | import FormData from 'form-data' 4 | /** 5 | * 上传图片至服务器,返回指定 type 的 imageId,url,及 path 6 | * @param option 选项 7 | * @param option.httpUrl mirai-api-http server 的地址 8 | * @param option.sessionKey 会话标识 9 | * @param option.type 'friend' 或 'group' 或 'temp' 10 | * @param option.img 图片二进制数据 11 | * @param option.suffix 图片文件类型 12 | * @returns Image或FlashImage对象 13 | */ 14 | export default async ({ 15 | httpUrl, 16 | sessionKey, 17 | type, 18 | img, 19 | suffix 20 | }: { 21 | httpUrl: string 22 | sessionKey: string 23 | type: 'friend' | 'group' | 'temp' 24 | img: Buffer 25 | suffix: string 26 | }): Promise<{ imageId: string; url: string; path: '' }> => { 27 | // 构造 fromdata 28 | const form = new FormData() 29 | form.append('sessionKey', sessionKey) 30 | form.append('type', type) 31 | // filename 指定了文件名 32 | form.append('img', img, `image.${suffix}`) 33 | // 请求 34 | const responseData = await axios.post<{ 35 | msg: string 36 | code: number 37 | imageId: string 38 | url: string 39 | }>(new URL('/uploadImage', httpUrl).toString(), form, { 40 | // formdata.getHeaders 将会指定 content-type,同时给定随 41 | // 机生成的 boundary,即分隔符,用以分隔多个表单项而不会造成混乱 42 | headers: form.getHeaders() 43 | }) 44 | const { 45 | data: { msg: message, code, imageId, url } 46 | } = responseData 47 | // 抛出 mirai 的异常 48 | if (code && code != 0) { 49 | throw new MiraiError(code, message) 50 | } 51 | return { 52 | imageId, 53 | url, 54 | path: '' 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/core/uploadVoice.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../Error' 2 | import { Voice } from '../Message' 3 | import axios from 'axios' 4 | import FormData from 'form-data' 5 | /** 6 | * 上传图片至服务器,返回指定 type 的 imageId,url,及 path 7 | * @param option 选项 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.type 'friend' 或 'group' 或 'temp' 11 | * @param option.voice 语音二进制数据 12 | * @param option.suffix 语音文件类型 13 | * @returns Voice对象 14 | */ 15 | export default async ({ 16 | httpUrl, 17 | sessionKey, 18 | type, 19 | voice, 20 | suffix 21 | }: { 22 | httpUrl: string 23 | sessionKey: string 24 | type: 'friend' | 'group' | 'temp' 25 | voice: Buffer 26 | suffix: string 27 | }): Promise => { 28 | // 构造 fromdata 29 | const form = new FormData() 30 | form.append('sessionKey', sessionKey) 31 | form.append('type', type) 32 | // filename 指定了文件名 33 | form.append('voice', voice, `audio.${suffix}`) 34 | // 请求 35 | const responseData = await axios.post<{ 36 | msg: string 37 | code: number 38 | voiceId: string 39 | url: string 40 | }>(new URL('/uploadVoice', httpUrl).toString(), form, { 41 | // formdata.getHeaders 将会指定 content-type,同时给定随 42 | // 机生成的 boundary,即分隔符,用以分隔多个表单项而不会造成混乱 43 | headers: form.getHeaders() 44 | }) 45 | const { 46 | data: { msg: message, code, voiceId, url } 47 | } = responseData 48 | // 抛出 mirai 的异常 49 | if (code && code != 0) { 50 | throw new MiraiError(code, message) 51 | } 52 | return new Voice({ 53 | voiceId, 54 | url, 55 | path: '' 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/core/fs/uploadFile.ts: -------------------------------------------------------------------------------- 1 | import { MiraiError } from '../../Error' 2 | import axios from 'axios' 3 | import { GroupID, UserID } from '../../Base' 4 | import FormData from 'form-data' 5 | /** 6 | * 上传文件到服务器 7 | * @param option 选项 8 | * @param option.httpUrl mirai-api-http server 的地址 9 | * @param option.sessionKey 会话标识 10 | * @param option.type 'friend' 或 'group'。 11 | * @param option.target 群号 12 | * @param option.path 上传文件路径 13 | * @param option.file 文件二进制数据 14 | * @returns 文件id 15 | * 16 | * FIXME:mirai-api-http咕了,我们没有咕!目前仅支持group。 17 | */ 18 | export default async ({ 19 | httpUrl, 20 | sessionKey, 21 | target, 22 | type, 23 | path, 24 | file, 25 | filename 26 | }: { 27 | httpUrl: string 28 | sessionKey: string 29 | type: 'friend' | 'group' 30 | target: GroupID | UserID 31 | path: string 32 | file: Buffer 33 | filename: string 34 | }): Promise<{ path: string; name: string }> => { 35 | // 构造 fromdata 36 | const form = new FormData() 37 | form.append('sessionKey', sessionKey) 38 | form.append('type', type) 39 | form.append('target', target) 40 | form.append('path', path) 41 | form.append('file', file, { filename }) 42 | // 请求 43 | const responseData = await axios.post<{ 44 | code: number 45 | msg: string 46 | data: { 47 | name: string 48 | path: string 49 | } 50 | }>(new URL('/file/upload', httpUrl).toString(), form, { 51 | // formdata.getHeaders 将会指定 content-type,同时给定随 52 | // 机生成的 boundary,即分隔符,用以分隔多个表单项而不会造成混乱 53 | headers: form.getHeaders() 54 | }) 55 | const { 56 | data: { msg: message, code, data } 57 | } = responseData 58 | // 抛出 mirai 的异常 59 | if (code && code != 0) { 60 | throw new MiraiError(code, message) 61 | } 62 | return { name: data.name, path: data.path } 63 | } 64 | -------------------------------------------------------------------------------- /src/core/listenBegin.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | import { EventBase, WsClose, WsError, WsUnexpectedResponse } from '../Event' 3 | /** 4 | * 开始侦听事件 5 | * @param option 设定 6 | * @param option.wsUrl ws链接 7 | * @param option.sessionKey sessionKey 8 | * @param option.message 回调函数 9 | * @param option.error 回调函数 10 | * @param option.close 回调函数 11 | * @param option.unexpectedResponce 回调函数 12 | * @returns 建立连接的 WebSocket 实例 13 | */ 14 | export default async ({ 15 | wsUrl, 16 | sessionKey, 17 | verifyKey, 18 | message, 19 | error, 20 | close, 21 | unexpectedResponse 22 | }: { 23 | wsUrl: string 24 | sessionKey: string 25 | verifyKey: string 26 | message: (data: EventBase) => void 27 | error: (err: WsError) => void 28 | close: ({ code, reason }: WsClose) => void 29 | unexpectedResponse: (obj: WsUnexpectedResponse) => void 30 | }): Promise => { 31 | const ws = new WebSocket( 32 | new URL( 33 | `/all?sessionKey=${sessionKey}&verifyKey=${verifyKey}`, 34 | wsUrl 35 | ).toString() 36 | ) 37 | // 监听 ws 事件,分发消息 38 | ws.on('open', () => { 39 | // 60s heartbeat 40 | const interval = setInterval(() => { 41 | ws.ping((err: Error): void => { 42 | if (err) console.error('ws ping error', err) 43 | }) 44 | }, 60000) 45 | ws.on('message', (data: Buffer) => 46 | message(JSON.parse(data.toString())?.data) 47 | ) 48 | ws.on('error', (err: Error) => error({ type: 'error', error: err })) 49 | ws.on('close', (code, reason) => { 50 | // 关闭心跳 51 | clearInterval(interval) 52 | close({ type: 'close', code, reason }) 53 | }) 54 | ws.on('unexpected-response', (req, res) => 55 | unexpectedResponse({ 56 | type: 'unexpected-response', 57 | request: req, 58 | response: res 59 | }) 60 | ) 61 | }) 62 | return ws 63 | } 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "module": "ESNext", 5 | "noImplicitAny": true, 6 | "removeComments": false /* Do not emit comments to output. */, 7 | // "noEmit": true, /* Do not emit outputs. */ 8 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 9 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 10 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 11 | /* Strict Type-Checking Options */ 12 | "strict": true /* Enable all strict type-checking options. */, 13 | "strictNullChecks": true /* Enable strict null checks. */, 14 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 15 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 16 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 17 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 18 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 19 | /* Additional Checks */ 20 | "noUnusedLocals": true /* Report errors on unused locals. */, 21 | "noUnusedParameters": true /* Report errors on unused parameters. */, 22 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 23 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 24 | /* Module Resolution Options */ 25 | "moduleResolution": "node", 26 | "esModuleInterop": true, 27 | "baseUrl": ".", 28 | "paths": { 29 | // "~/*": [ 30 | // "./types/*" 31 | // ] 32 | }, 33 | "target": "ESNext", 34 | "outDir": "dist", 35 | "declaration": true 36 | }, 37 | "include": ["src/**/*", "tsup.config.ts"] 38 | } 39 | -------------------------------------------------------------------------------- /src/Waiter.ts: -------------------------------------------------------------------------------- 1 | import { MemberID, UserID } from './Base' 2 | import { GroupMessage, TempMessage, Message, EventArg } from './Event' 3 | import { Matcher } from './Bot' 4 | interface map { 5 | friend: 'FriendMessage' 6 | member: 'GroupMessage' 7 | temp: 'TempMessage' | 'StrangerMessage' 8 | } 9 | /** 10 | * Bot.wait方法的匹配器生成器。可以利用此方法来等待指定用户的下一条消息。 11 | * @param type 匹配器类型。'friend'用于好友消息,'member'用于群聊消息,'temp'用于临时消息和陌生人消息。 12 | * @param qq 用户qq,群号或上下文。 13 | * @param extend 要附加的匹配器。 14 | * @returns 匹配器 15 | */ 16 | export function Waiter( 17 | type: 'friend', 18 | qq: UserID, 19 | extend?: Matcher<'FriendMessage'> 20 | ): Matcher<'FriendMessage'> 21 | export function Waiter( 22 | type: 'member', 23 | qq: MemberID, 24 | extend?: Matcher<'GroupMessage'> 25 | ): Matcher<'GroupMessage'> 26 | export function Waiter( 27 | type: 'temp', 28 | qq: MemberID, 29 | extend?: Matcher<'TempMessage'> 30 | ): Matcher<'TempMessage'> 31 | export function Waiter( 32 | type: 'temp', 33 | qq: UserID, 34 | extend?: Matcher<'StrangerMessage'> 35 | ): Matcher<'StrangerMessage'> 36 | export function Waiter( 37 | type: T, 38 | qq: MemberID | UserID, 39 | extend?: Matcher 40 | ): Matcher { 41 | if (type == 'friend' && typeof qq == 'number') { 42 | return (data: Message): boolean => 43 | data.sender.id == qq && (!extend || extend(data as EventArg)) 44 | } else if (type == 'temp') { 45 | if (typeof qq == 'number') 46 | return (data: Message): boolean => 47 | data.sender.id == qq && (!extend || extend(data as EventArg)) 48 | return (data: Message): boolean => 49 | data.sender.id == qq.qq && 50 | (data as TempMessage).sender.group.id == qq.group && 51 | (!extend || extend(data as EventArg)) 52 | } else if (type == 'member' && typeof qq != 'number') { 53 | return (data: Message): boolean => 54 | data.sender.id == qq.qq && 55 | (data as GroupMessage).sender.group.id == qq.group && 56 | (!extend || extend(data as EventArg)) 57 | } else throw new Error('Waiter 参数错误') 58 | } 59 | -------------------------------------------------------------------------------- /src/Base.ts: -------------------------------------------------------------------------------- 1 | /** 上下文,用于指定群聊成员。 */ 2 | export interface MemberID { 3 | group: GroupID 4 | qq: UserID 5 | } 6 | /** 用户qq。 */ 7 | export type UserID = number 8 | /** 群聊qq。 */ 9 | export type GroupID = number 10 | /** 公告的结构。 */ 11 | export interface Announcement { 12 | /** 公告所在的群聊。 */ 13 | group: Group 14 | /** 公告内容 */ 15 | content: string 16 | /** 创建公告的用户 */ 17 | senderId: UserID 18 | /** 公告唯一id */ 19 | fid: string 20 | /** 是否全员确认 */ 21 | allConfirmed: boolean 22 | /** 已确认的人数 */ 23 | confirmedMembersCount: number 24 | /** 公告发布的时间 */ 25 | publicationTime: number 26 | } 27 | /** 权限类型,群主/管理员/成员 */ 28 | export type Permission = 'OWNER' | 'ADMINISTRATOR' | 'MEMBER' 29 | /** 群组。 */ 30 | export interface Group { 31 | /** 群号 */ 32 | id: GroupID 33 | /** 群名称 */ 34 | name: string 35 | /** Bot在群内的权限 */ 36 | permission: Permission 37 | } 38 | /** 成员 */ 39 | export interface Member { 40 | /** 成员qq */ 41 | id: UserID 42 | /** 成员群名片 */ 43 | memberName: string 44 | /** 成员群头衔 */ 45 | specialTitle: string 46 | /** 成员的权限 */ 47 | permission: Permission 48 | /** 成员加入群组的时间 */ 49 | joinTimestamp: number 50 | /** 最后发言的时间 */ 51 | lastSpeakTimestamp: number 52 | /** 禁言剩余的时间 */ 53 | muteTimeRemaining: number 54 | /** 成员所在的群聊 */ 55 | group: Group 56 | } 57 | /** 用户 */ 58 | export interface User { 59 | /** 用户qq */ 60 | id: UserID 61 | /** 用户昵称 */ 62 | nickname: string 63 | /** 用户备注 */ 64 | remark: string 65 | } 66 | /** 其它平台客户端 */ 67 | export interface OtherClient { 68 | /** 平台id */ 69 | id: number 70 | /** 平台类型 */ 71 | platform: string 72 | } 73 | /** 用户的资料 */ 74 | export interface Profile { 75 | /** 昵称 */ 76 | nickname: string 77 | /** 电子邮箱 */ 78 | email: string 79 | /** 年龄 */ 80 | age: number 81 | /** QQ等级 */ 82 | level: number 83 | /** 用户个性签名 */ 84 | sign: number 85 | /** 用户性别 */ 86 | sex: Sex 87 | } 88 | /** 群聊信息 89 | * 注:没有群公告是因为这个东西可能被弃用了而没有在API上更新。 90 | * 如果认为需要加回的情况,请开issue。 91 | */ 92 | export interface GroupInfo { 93 | /** 群聊名称 */ 94 | name: string 95 | /** 允许群内坦白说 */ 96 | confessTalk: boolean 97 | /** 允许邀请进群 */ 98 | allowMemberInvite: boolean 99 | /** 自动审核 */ 100 | autoApprove: boolean 101 | /** 允许匿名聊天 */ 102 | anonymousChat: boolean 103 | } 104 | export interface MemberSetting { 105 | /** 成员名字 */ 106 | memberName?: string 107 | /** 群头衔 */ 108 | specialTitle?: string 109 | /** 权限 */ 110 | permission?: 'ADMINISTRATOR' | 'MEMBER' 111 | } 112 | /** 用户性别 */ 113 | export type Sex = 'UNKNOWN' | 'MALE' | 'FEMALE' 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mirai-foxes 2 | 3 | [![CI Status](https://github.com/FurDevsCN/mirai-foxes/actions/workflows/typescript.yml/badge.svg)](https://github.com/FurDevsCN/mirai-foxes/actions/workflows/typescript.yml) 4 | [![CodeFactor](https://www.codefactor.io/repository/github/FurDevsCN/mirai-foxes/badge)](https://www.codefactor.io/repository/github/FurDevsCN/mirai-foxes) 5 | [![HitCount](https://hits.dwyl.com/FurDevsCN/mirai-foxes.svg?style=flat-square)](http://hits.dwyl.com/FurDevsCN/mirai-foxes) 6 | 7 | mirai-foxes,一个根据 [Mirai-js](https://github.com/Drincann/Mirai-js) 代码重写,运行在 Node.js 下的 Typescript QQ 机器人开发框架。 8 | 9 | 在 Mirai-js 代码的基础上进行了一些修正,总体提高了代码的可读性,并添加了几个新功能(~~还从别的框架抄了点功能过来~~)。 10 | 11 | 适合那些觉得 mirai-ts 不够顺手又无法忍受 Mirai-js 类型注释不足的 Devs。同样也适合 Mirai-js 用户! 12 | 13 | 如果你希望体验完整的 demo,看看 [foxes-awesome](https://github.com/FurDevsCN/foxes-awesome)。 14 | 15 | ```typescript 16 | import { Middleware, Bot, Message, Event } from 'mirai-foxes' 17 | bot.on( 18 | 'FriendMessage', 19 | Middleware.Middleware({ 20 | filter: ['user', [Middleware.userFilter([0])]], 21 | parser: [Middleware.parseCmd], 22 | matcher: [Middleware.cmdMatch('hello')] 23 | })(async (data: Event.FriendMessage) => { 24 | await bot.send('friend', { 25 | qq: data.sender.id, 26 | message: [new Message.Plain('Hello World!')] 27 | }) 28 | }) 29 | ) 30 | ``` 31 | 32 | # 开发文档 33 | 34 | mirai-foxes 开发文档 -> [https://furdevscn.github.io/mirai-foxes](https://furdevscn.github.io/mirai-foxes) 35 | 36 | # 比较 37 | 38 | [mirai-ts](https://github.com/YunYouJun/mirai-ts): 39 | 40 | > 类型注释和本项目相似(更好?),内部代码优雅程度比本项目好(有时候也可能是本项目比 mirai-ts 好,~~开发者本当会写 Typescript~~),但是本项目的构造参数写得比 mirai-ts 好,支持监听的~~冷门~~事件也比 mirai-ts 稍微多一点点。 41 | 42 | [node-mirai](https://github.com/RedBeanN/node-mirai): 43 | 44 | > 泛用性较本项目好(只需变更 listen 就可以完成对群组/用户消息的监听),类型注释较本项目略为不足,接口较本项目复杂。 45 | 46 | [Mirai-js](https://github.com/Drincann/Mirai-js): 47 | 48 | > 本项目较 Mirai-js 多出了完整的类型注释,但本项目的 Middleware 较 Mirai-js 功能更弱(我懒得重写,~~要写暑假作业了~~)(~~加上数据放 data 里的方式太迷惑了~~),仅有部分简单中间件(~~借鉴 Ariadne~~),虽然能满足基本的命令匹配需求,但操作起来可能仍然比较棘手。 49 | 50 | [Graia-Ariadne / Graia-Avilla](https://graiax.cn) 51 | 52 | > mirai-foxes 论功能上完全不如 Graia。 53 | > 54 | > mirai-foxes 仅模仿了 Graia 的小部分基本功能,这样可以在保持不错可读性的同时,降低学习成本。 55 | > 56 | > 我们更推荐在 Python 机器人编程中使用 Graia。 57 | 58 | ## 支持这个项目 59 | 60 | 我是[FurDevsCN](https://github.com/FurDevsCN)开发组的一名成员。如果您希望支持这个项目,可以访问我们的页面来获得更多信息(~~总不可能让我去开 Patreon 或者爱发电吧~~)。 61 | 62 | 如果觉得这个项目还不错的话,就动动小手给个 star 吧! 63 | 64 | ## 感谢 65 | 66 | ### hyperextensible Vim-based text editor 67 | 68 | 本项目使用免费且自由的 [Neovim](https://neovim.io/) 完成开发。 69 | 70 | 同时也推荐您使用 [JetBrains](https://www.jetbrains.com/) 开发工具。 71 | 72 | 或者您也可以看看不错的 [Visual Studio Code](https://code.visualstudio.com/)。 73 | 74 | **Graia Framework** 的开发者为此框架的 API 设计提出了重要建议,在此表示感谢。 75 | 76 | **Nullqwertyuiop** 是文档的重要示范对象。由于文档是由多人编辑的,且未经过质量审查,难免包含有对其本人冒犯性的语句。在此对他的包容表示由衷感谢。此外,他还负责了 mirai-foxes 的文档部署,非常感谢。 77 | 78 | **FurryHome** 为此框架的 debug 提供了部分支援。在此表示感谢。 79 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: ['master'] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ['master'] 20 | schedule: 21 | - cron: '31 10 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 52 | # queries: security-extended,security-and-quality 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 60 | 61 | # If the Autobuild fails above, remove it and uncomment the following three lines. 62 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 63 | 64 | # - run: | 65 | # echo "Run, Build Application using script" 66 | # ./location_of_script_within_repo/buildscript.sh 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@v2 70 | -------------------------------------------------------------------------------- /src/Message.ts: -------------------------------------------------------------------------------- 1 | import { GroupID, UserID } from './Base' 2 | /** 消息类型 */ 3 | export type MessageType = 4 | | 'Source' 5 | | 'Quote' 6 | | 'Plain' 7 | | 'At' 8 | | 'AtAll' 9 | | 'Image' 10 | | 'FlashImage' 11 | | 'Voice' 12 | | 'Xml' 13 | | 'Json' 14 | | 'App' 15 | | 'Poke' 16 | | 'Dice' 17 | | 'MarketFace' 18 | | 'MusicShare' 19 | | 'Forward' 20 | | 'Face' 21 | | 'File' 22 | /** 基本消息类型,请在判断类型后用as转换为派生类 */ 23 | export class MessageBase { 24 | readonly type: MessageType 25 | constructor({ type }: { type: MessageType }) { 26 | this.type = type 27 | } 28 | } 29 | /** 消息源信息,用于回复或消息操作 */ 30 | export class Source extends MessageBase { 31 | /** 消息id */ 32 | id: number 33 | /** 发送时间 */ 34 | time: number 35 | constructor({ id, time }: { id: number; time: number }) { 36 | super({ type: 'Source' }) 37 | ;[this.id, this.time] = [id, time] 38 | } 39 | } 40 | /** 引用信息 */ 41 | export class Quote extends MessageBase { 42 | /** 原消息的id */ 43 | id: number 44 | /** 群组id */ 45 | groupId: 0 | GroupID 46 | /** 原消息发送人id */ 47 | senderId: UserID 48 | /** 原消息的目标ID */ 49 | targetId: GroupID | UserID 50 | /** 原消息内容(不带Source) */ 51 | origin: MessageBase[] 52 | constructor({ 53 | id, 54 | groupId, 55 | senderId, 56 | targetId, 57 | origin 58 | }: { 59 | id: number 60 | groupId: GroupID 61 | senderId: UserID 62 | targetId: UserID 63 | origin: MessageBase[] 64 | }) { 65 | super({ type: 'Quote' }) 66 | ;[this.id, this.groupId, this.senderId, this.targetId, this.origin] = [ 67 | id, 68 | groupId, 69 | senderId, 70 | targetId, 71 | origin 72 | ] 73 | } 74 | } 75 | /** 纯文本 */ 76 | export class Plain extends MessageBase { 77 | /** 消息文字 */ 78 | text: string 79 | constructor(text: string) { 80 | super({ type: 'Plain' }) 81 | this.text = text 82 | } 83 | } 84 | /** at消息 */ 85 | export class At extends MessageBase { 86 | /** 目标用户ID */ 87 | target: UserID 88 | /** 他人显示的内容(被At用户的群名片) */ 89 | display: string 90 | constructor(target: UserID, display = '') { 91 | super({ type: 'At' }) 92 | // display=客户端表示的内容 93 | ;[this.target, this.display] = [target, display] 94 | } 95 | } 96 | /** at全体成员消息 */ 97 | export class AtAll extends MessageBase { 98 | constructor() { 99 | super({ type: 'AtAll' }) 100 | } 101 | } 102 | /** 图片消息 */ 103 | export class Image extends MessageBase { 104 | /** 图片id */ 105 | imageId: string 106 | /** 下载图片的url */ 107 | url: string 108 | /** 本地路径,相对于JVM工作路径(不推荐) */ 109 | path: string | undefined 110 | constructor({ 111 | imageId = '', 112 | url = '', 113 | path 114 | }: { 115 | imageId?: string 116 | url?: string 117 | path?: string 118 | }) { 119 | super({ type: 'Image' }) 120 | ;[this.imageId, this.url, this.path] = [imageId, url, path] 121 | } 122 | } 123 | /** 闪图(阅后即焚) */ 124 | export class FlashImage extends MessageBase { 125 | /** 图片id */ 126 | imageId: string 127 | /** 下载图片的url */ 128 | url: string 129 | /** 本地路径,相对于JVM工作路径(不推荐) */ 130 | path?: string 131 | constructor({ 132 | imageId = '', 133 | url = '', 134 | path 135 | }: { 136 | imageId?: string 137 | url?: string 138 | path?: string 139 | }) { 140 | super({ type: 'FlashImage' }) 141 | ;[this.imageId, this.url, this.path] = [imageId, url, path] 142 | } 143 | } 144 | /** 语音 */ 145 | export class Voice extends MessageBase { 146 | /** 语音id */ 147 | voiceId: string 148 | /** 下载路径 */ 149 | url: string 150 | /** 本地路径,相对于JVM工作路径(不推荐) */ 151 | path: string 152 | constructor({ 153 | voiceId = '', 154 | url = '', 155 | path = '' 156 | }: { 157 | voiceId?: string 158 | url?: string 159 | path?: string 160 | }) { 161 | super({ type: 'Voice' }) 162 | // 语音路径相对于 mirai-console 163 | // 的 plugins/MiraiAPIHTTP/voices 164 | ;[this.voiceId, this.url, this.path] = [voiceId, url, path] 165 | } 166 | } 167 | /** 戳一戳 */ 168 | export class Poke extends MessageBase { 169 | /** 戳一戳类型 */ 170 | name: string 171 | constructor({ name }: { name: string }) { 172 | super({ type: 'Poke' }) 173 | this.name = name 174 | } 175 | } 176 | /** 骰子消息 */ 177 | export class Dice extends MessageBase { 178 | /** 扔出来的点数 */ 179 | value: number 180 | constructor({ value }: { value: number }) { 181 | super({ type: 'Dice' }) 182 | this.value = value 183 | } 184 | } 185 | /** 商城表情 */ 186 | export class MarketFace extends MessageBase { 187 | /** 表情唯一id */ 188 | id: number 189 | /** 表情显示名称 */ 190 | name: string 191 | constructor({ id, name }: { id: number; name: string }) { 192 | super({ type: 'MarketFace' }) 193 | ;[this.id, this.name] = [id, name] 194 | } 195 | } 196 | /** 音乐分享 */ 197 | export class MusicShare extends MessageBase { 198 | /** 类型 */ 199 | kind: string 200 | /** 标题 */ 201 | title: string 202 | /** 概要 */ 203 | summary: string 204 | /** 点击后跳转的链接 */ 205 | jumpUrl: string 206 | /** 图片链接 */ 207 | pictureUrl: string 208 | /** 音源(不点进去而是点播放时播放的声音)链接 */ 209 | musicUrl: string 210 | /** 简介 */ 211 | brief: string 212 | constructor({ 213 | kind, 214 | title, 215 | summary, 216 | jumpUrl, 217 | pictureUrl, 218 | musicUrl, 219 | brief 220 | }: { 221 | kind: string 222 | title: string 223 | summary: string 224 | jumpUrl: string 225 | pictureUrl: string 226 | musicUrl: string 227 | brief: string 228 | }) { 229 | super({ type: 'MusicShare' }) 230 | ;[ 231 | this.kind, 232 | this.title, 233 | this.summary, 234 | this.jumpUrl, 235 | this.pictureUrl, 236 | this.musicUrl, 237 | this.brief 238 | ] = [kind, title, summary, jumpUrl, pictureUrl, musicUrl, brief] 239 | } 240 | } 241 | /** 文件 */ 242 | export class File extends MessageBase { 243 | /** 文件id(用来获得文件) */ 244 | id: string 245 | /** 文件名 */ 246 | name: string 247 | /** 大小 */ 248 | size: number 249 | constructor({ id, name, size }: { id: string; name: string; size: number }) { 250 | super({ type: 'File' }) 251 | ;[this.id, this.name, this.size] = [id, name, size] 252 | } 253 | } 254 | /** Xml卡片 */ 255 | export class Xml extends MessageBase { 256 | /** xml内容 */ 257 | xml: string 258 | constructor(xml: string) { 259 | super({ type: 'Xml' }) 260 | this.xml = xml 261 | } 262 | } 263 | /** Json卡片 */ 264 | export class Json extends MessageBase { 265 | /** json内容 */ 266 | json: string 267 | constructor(json: string) { 268 | super({ type: 'Json' }) 269 | this.json = json 270 | } 271 | } 272 | /** 小程序 */ 273 | export class App extends MessageBase { 274 | /** 小程序内容 */ 275 | content: string 276 | constructor(content: string) { 277 | super({ type: 'App' }) 278 | this.content = content 279 | } 280 | } 281 | /** 转发消息节点 */ 282 | export interface ForwardNode { 283 | /** 发送者id */ 284 | senderId?: UserID 285 | /** 发送时间 */ 286 | time?: number 287 | /** 发送者名称 */ 288 | senderName?: string 289 | /** 原消息链 */ 290 | messageChain?: MessageBase[] 291 | /** 可以指定messageId而不是节点 */ 292 | messageId?: number 293 | } 294 | /** 转发消息 */ 295 | export class ForwardNodeList extends MessageBase { 296 | /** 节点 */ 297 | nodeList: ForwardNode[] 298 | /** 299 | * 添加一个节点 300 | * @param node 可以是ForwardNode也可以是messageId 301 | * @returns this 302 | */ 303 | add(node: ForwardNode | number): this { 304 | if (typeof node == 'number') { 305 | this.nodeList.push({ 306 | messageId: node 307 | }) 308 | } else { 309 | this.nodeList.push(node) 310 | } 311 | return this 312 | } 313 | constructor(nodeList: ForwardNode[]) { 314 | super({ type: 'Forward' }) 315 | this.nodeList = nodeList 316 | } 317 | } 318 | /** 表情 */ 319 | export class Face extends MessageBase { 320 | /** 表情ID */ 321 | faceId: number 322 | /** 表情名称 */ 323 | name: string 324 | constructor({ faceId, name }: { faceId: number; name: string }) { 325 | super({ 326 | type: 'Face' 327 | }) 328 | this.faceId = faceId 329 | this.name = name 330 | } 331 | } 332 | /** 消息链类型 */ 333 | export type MessageChain = MessageBase[] & { 334 | 0: Source 335 | } 336 | -------------------------------------------------------------------------------- /src/File.ts: -------------------------------------------------------------------------------- 1 | import { Group, GroupID, User, UserID } from './Base' 2 | import _getGroupFileList from './core/fs/getGroupFileList' 3 | import _uploadFile from './core/fs/uploadFile' 4 | import _deleteFile from './core/fs/deleteFile' 5 | import _renameGroupFile from './core/fs/renameGroupFile' 6 | import _getGroupFileInfo from './core/fs/getGroupFileInfo' 7 | import _moveGroupFile from './core/fs/moveGroupFile' 8 | import _makeDirectory from './core/fs/makeDirectory' 9 | import _getDownloadInfo from './core/fs/getDownloadInfo' 10 | import { Bot } from './Bot' 11 | async function getGroupFileList({ 12 | httpUrl, 13 | sessionKey, 14 | path, 15 | group 16 | }: { 17 | httpUrl: string 18 | sessionKey: string 19 | path: string 20 | group: GroupID 21 | }): Promise { 22 | let offset = 0 23 | let temp: FileDetail[] = [] 24 | const fileList: FileDetail[] = [] 25 | while ( 26 | (temp = await _getGroupFileList({ 27 | httpUrl, 28 | sessionKey, 29 | group, 30 | path, 31 | offset, 32 | size: 10 33 | })).length > 0 34 | ) { 35 | fileList.push(...temp) 36 | // 获取下一页 37 | offset += 10 38 | } 39 | return fileList 40 | } 41 | function getInstance( 42 | raw: FileDetail, 43 | { bot, target }: { bot: Bot; target: GroupID | UserID } 44 | ): File | Directory { 45 | if (raw.isFile == true) { 46 | return new File(bot, target, raw) 47 | } 48 | return new Directory(bot, target, raw) 49 | } 50 | export interface DownloadInfo { 51 | // sha1 52 | sha1: string 53 | // md5 54 | md5: string 55 | // 下载次数 56 | downloadTimes: number 57 | // 上传者QQ 58 | uploaderId: UserID 59 | // 上传时间 60 | uploadTime: number 61 | // 最后修改时间 62 | lastModifyTime: number 63 | // 下文件的url 64 | url: string 65 | } 66 | export interface FileDetail { 67 | // 文件名 68 | name: string 69 | // id 70 | id: string 71 | // 路径 72 | path: string 73 | // 父目录 74 | parent: null | FileDetail 75 | // 来自 76 | contact: Group | User 77 | // 是否是文件 78 | isFile: boolean 79 | // 是否是目录(可和文件二选一,统一用一个) 80 | isDirectory: boolean 81 | // sha1 82 | sha1: string 83 | // md5 84 | md5: string 85 | // 下载次数 86 | downloadTimes: number 87 | // 上传者QQ 88 | uploaderId: UserID 89 | // 上传时间 90 | uploadTime: number 91 | // 最后修改时间 92 | lastModifyTime: number 93 | } 94 | export class FileManager { 95 | private bot: Bot 96 | private target: GroupID | UserID 97 | /** 98 | * 列出根目录的文件/文件夹 99 | * @returns 文件/目录 数组 100 | */ 101 | async list(): Promise<(File | Directory)[]> { 102 | return ( 103 | await getGroupFileList({ 104 | httpUrl: this.bot.config.httpUrl, 105 | sessionKey: this.bot.config.sessionKey, 106 | group: this.target, 107 | path: '/' 108 | }) 109 | ).map((fileObj: FileDetail) => 110 | getInstance(fileObj, { 111 | bot: this.bot, 112 | target: this.target 113 | }) 114 | ) 115 | } 116 | /** 117 | * 由id获取文件或目录 118 | * @param id id 119 | * @returns 文件/目录 120 | */ 121 | async id(id: string): Promise { 122 | return getInstance( 123 | await _getGroupFileInfo({ 124 | httpUrl: this.bot.config.httpUrl, 125 | sessionKey: this.bot.config.sessionKey, 126 | group: this.target, 127 | id, 128 | path: null 129 | }), 130 | { 131 | bot: this.bot, 132 | target: this.target 133 | } 134 | ) 135 | } 136 | /** 137 | * 获取文件/目录 138 | * @param name 名字/id。 139 | * @returns 文件/目录 140 | */ 141 | async get(name: string): Promise { 142 | return getInstance( 143 | await _getGroupFileInfo({ 144 | httpUrl: this.bot.config.httpUrl, 145 | sessionKey: this.bot.config.sessionKey, 146 | group: this.target, 147 | path: '/' + name 148 | }), 149 | { 150 | bot: this.bot, 151 | target: this.target 152 | } 153 | ) 154 | } 155 | /** 156 | * 创建文件夹 157 | * @param name 文件夹名 158 | */ 159 | async mkdir(name: string): Promise { 160 | return getInstance( 161 | await _makeDirectory({ 162 | httpUrl: this.bot.config.httpUrl, 163 | sessionKey: this.bot.config.sessionKey, 164 | group: this.target, 165 | path: '/', 166 | directoryName: name 167 | }), 168 | { 169 | bot: this.bot, 170 | target: this.target 171 | } 172 | ) as Directory 173 | } 174 | /** 175 | * 上传文件 176 | * @param file 文件二进制数据 177 | * @param filename 文件名 178 | */ 179 | async upload(file: Buffer, filename: string): Promise { 180 | const f = await _uploadFile({ 181 | httpUrl: this.bot.config.httpUrl, 182 | sessionKey: this.bot.config.sessionKey, 183 | target: this.target, 184 | type: 'group', 185 | filename, 186 | path: '/', 187 | file 188 | }) 189 | return getInstance( 190 | await _getGroupFileInfo({ 191 | httpUrl: this.bot.config.httpUrl, 192 | sessionKey: this.bot.config.sessionKey, 193 | group: this.target, 194 | path: f.path 195 | }), 196 | { 197 | bot: this.bot, 198 | target: this.target 199 | } 200 | ) as File 201 | } 202 | constructor(bot: Bot, target: GroupID | UserID) { 203 | void ([this.bot, this.target] = [bot, target]) 204 | } 205 | } 206 | export class File { 207 | private bot: Bot 208 | private target: GroupID 209 | private _detail: FileDetail 210 | /** 211 | * 获得下载信息 212 | */ 213 | async download(): Promise { 214 | return await _getDownloadInfo({ 215 | httpUrl: this.bot.config.httpUrl, 216 | sessionKey: this.bot.config.sessionKey, 217 | path: this._detail.path, 218 | group: this.target 219 | }) 220 | } 221 | /** 222 | * 移除该文件 223 | */ 224 | async remove(): Promise { 225 | // TODO:mah 支持不完全,及时跟进 226 | await _deleteFile({ 227 | httpUrl: this.bot.config.httpUrl, 228 | sessionKey: this.bot.config.sessionKey, 229 | target: this.target, 230 | type: 'group', 231 | path: this._detail.path 232 | }) 233 | } 234 | /** 235 | * 移动文件 236 | * @param dir 要移动到的文件夹。 237 | * @returns this 238 | */ 239 | async move(dir: Directory): Promise { 240 | await _moveGroupFile({ 241 | httpUrl: this.bot.config.httpUrl, 242 | sessionKey: this.bot.config.sessionKey, 243 | group: this.target, 244 | path: this._detail.path, 245 | moveToPath: dir.detail.path 246 | }) 247 | // 更新 detail 248 | await this.update() 249 | return this 250 | } 251 | /** 252 | * 重命名文件 253 | * @param name 新名字 254 | * @returns this 255 | */ 256 | async rename(name: string): Promise { 257 | await _renameGroupFile({ 258 | httpUrl: this.bot.config.httpUrl, 259 | sessionKey: this.bot.config.sessionKey, 260 | group: this.target, 261 | path: this._detail.path, 262 | renameTo: name 263 | }) 264 | // 更新detail 265 | await this.update() 266 | return this 267 | } 268 | /** 269 | * 更新文件信息 270 | */ 271 | async update(): Promise { 272 | this._detail = await _getGroupFileInfo({ 273 | httpUrl: this.bot.config.httpUrl, 274 | sessionKey: this.bot.config.sessionKey, 275 | id: this._detail.id, 276 | path: null, 277 | group: this.target 278 | }) 279 | return this 280 | } 281 | /** 282 | * 文件属性 283 | */ 284 | get detail(): FileDetail { 285 | return this._detail 286 | } 287 | constructor(bot: Bot, target: GroupID, detail: FileDetail) { 288 | void ([this.bot, this.target, this._detail] = [bot, target, detail]) 289 | } 290 | } 291 | export class Directory { 292 | private bot: Bot 293 | private target: GroupID 294 | private _detail: FileDetail 295 | /** 296 | * 获取当前目录下的 文件/目录 数组 297 | */ 298 | async list(): Promise<(File | Directory)[]> { 299 | return ( 300 | await getGroupFileList({ 301 | httpUrl: this.bot.config.httpUrl, 302 | sessionKey: this.bot.config.sessionKey, 303 | group: this.target, 304 | path: this._detail.path 305 | }) 306 | ).map((fileObj: FileDetail) => 307 | getInstance(fileObj, { 308 | bot: this.bot, 309 | target: this.target 310 | }) 311 | ) 312 | } 313 | /** 314 | * 从当前目录获得文件/目录。 315 | * @param filename 文件/目录名。 316 | * @returns 文件/目录 317 | */ 318 | async get(filename: string): Promise { 319 | return getInstance( 320 | await _getGroupFileInfo({ 321 | httpUrl: this.bot.config.httpUrl, 322 | sessionKey: this.bot.config.sessionKey, 323 | group: this.target, 324 | path: this._detail.path + '/' + filename 325 | }), 326 | { 327 | bot: this.bot, 328 | target: this.target 329 | } 330 | ) 331 | } 332 | /** 333 | * 创建文件夹 334 | * @param name 文件夹名 335 | */ 336 | async mkdir(name: string): Promise { 337 | return getInstance( 338 | await _makeDirectory({ 339 | httpUrl: this.bot.config.httpUrl, 340 | sessionKey: this.bot.config.sessionKey, 341 | group: this.target, 342 | path: this._detail.path, 343 | directoryName: name 344 | }), 345 | { 346 | bot: this.bot, 347 | target: this.target 348 | } 349 | ) as Directory 350 | } 351 | /** 352 | * 上传文件至当前实例指代的目录下 353 | * @param file 二选一,文件二进制数据 354 | * @param filename 新的文件名。 355 | * @returns 文件。 356 | */ 357 | async upload(file: Buffer, filename: string): Promise { 358 | const f = await _uploadFile({ 359 | httpUrl: this.bot.config.httpUrl, 360 | sessionKey: this.bot.config.sessionKey, 361 | target: this.target, 362 | file, 363 | filename, 364 | type: 'group', 365 | path: this._detail.path 366 | }) 367 | return getInstance( 368 | await _getGroupFileInfo({ 369 | httpUrl: this.bot.config.httpUrl, 370 | sessionKey: this.bot.config.sessionKey, 371 | group: this.target, 372 | path: f.path 373 | }), 374 | { 375 | bot: this.bot, 376 | target: this.target 377 | } 378 | ) as File 379 | } 380 | /** 381 | * 文件属性 382 | */ 383 | get detail(): FileDetail { 384 | return this._detail 385 | } 386 | constructor(bot: Bot, target: GroupID, detail: FileDetail) { 387 | void ([this.bot, this.target, this._detail] = [bot, target, detail]) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/Middleware.ts: -------------------------------------------------------------------------------- 1 | import { GroupID, UserID } from './Base' 2 | import { FriendMessage, GroupMessage, OtherClientMessage } from './Event' 3 | import { Processor } from './Bot' 4 | import { At, MessageBase, MessageChain, MessageType, Plain } from './Message' 5 | type OneMessage = FriendMessage & GroupMessage & OtherClientMessage 6 | type AllMessage = FriendMessage | GroupMessage | OtherClientMessage 7 | type OneProcessor = 8 | | Processor<'FriendMessage'> 9 | | Processor<'GroupMessage'> 10 | | Processor<'OtherClientMessage'> 11 | type AllProcessor = Processor< 12 | 'FriendMessage' | 'GroupMessage' | 'OtherClientMessage' 13 | > 14 | class GroupFilter { 15 | private fn: ((data: GroupMessage) => boolean)[] = [] 16 | next(fn: ((data: GroupMessage) => boolean)[]): this { 17 | this.fn.push(...fn) 18 | return this 19 | } 20 | done(fn: Processor<'GroupMessage'>): Processor<'GroupMessage'> { 21 | return async (data: GroupMessage): Promise => { 22 | if (this.fn.every(fn => fn(data))) await fn(data) 23 | } 24 | } 25 | } 26 | /** 27 | * 设定群聊过滤器 28 | * @param id 群号数组。 29 | * @returns 特化中间件 30 | */ 31 | export function groupFilter(id: GroupID[]): (data: GroupMessage) => boolean { 32 | return (data: GroupMessage): boolean => { 33 | return id.includes(data.sender.group.id) 34 | } 35 | } 36 | /** 37 | * 设定成员过滤器 38 | * @param id 群号->成员QQ号的映射 39 | * @returns 特化中间件 40 | */ 41 | export function memberFilter( 42 | id: Map 43 | ): (data: GroupMessage) => boolean { 44 | return (data: GroupMessage): boolean => { 45 | if (id.has(data.sender.group.id)) { 46 | const q = id.get(data.sender.group.id) 47 | if (q && !q.includes(data.sender.id)) return false 48 | } 49 | return true 50 | } 51 | } 52 | class UserFilter { 53 | private fn: ((data: FriendMessage) => boolean)[] = [] 54 | next(fn: ((data: FriendMessage) => boolean)[]): this { 55 | this.fn.push(...fn) 56 | return this 57 | } 58 | done(fn: Processor<'FriendMessage'>): Processor<'FriendMessage'> { 59 | return async (data: FriendMessage): Promise => { 60 | if (this.fn.every(fn => fn(data))) await fn(data) 61 | } 62 | } 63 | } 64 | /** 65 | * 设定用户过滤器 66 | * @param id 群号数组。 67 | * @returns 特化中间件 68 | */ 69 | export function userFilter(id: UserID[]): (data: FriendMessage) => boolean { 70 | return (data: FriendMessage): boolean => { 71 | if (id.includes(data.sender.id)) return true 72 | return false 73 | } 74 | } 75 | function chain2str(v: MessageChain): string { 76 | let m = '' 77 | for (const d of v.slice(1)) { 78 | if (d.type == 'Plain') m += (d as Plain).text 79 | else m += `[${d.type}]` 80 | } 81 | return m 82 | } 83 | /** 84 | * 消息匹配处理器 85 | */ 86 | class Matcher { 87 | private fn: ((data: MessageChain) => boolean)[] = [] 88 | next(fn: ((data: MessageChain) => boolean)[]): this { 89 | this.fn.push(...fn) 90 | return this 91 | } 92 | done(fn: OneProcessor): AllProcessor { 93 | return async (data: AllMessage): Promise => { 94 | if (this.fn.every(fn => fn(data.messageChain))) 95 | await fn(data as OneMessage) 96 | } 97 | } 98 | } 99 | /** 100 | * 关键字(全文/模糊)匹配。 101 | * @param val 内容。 102 | * @returns 装饰器 103 | */ 104 | export function keywordMatch(val: string): (v: MessageChain) => boolean { 105 | return (v: MessageChain): boolean => chain2str(v).includes(val) 106 | } 107 | /** 108 | * 命令(除Source外第一个元素,且必须为文本)匹配。(推荐搭配命令解释器) 109 | * @param val 内容。 110 | * @returns 装饰器 111 | */ 112 | export function cmdMatch(val: string): (v: MessageChain) => boolean { 113 | return (v: MessageChain): boolean => 114 | v[1].type == 'Plain' && (v[1] as Plain).text == val 115 | } 116 | /** 117 | * 文本前缀匹配。 118 | * @param val 内容。 119 | * @returns 装饰器 120 | */ 121 | export function prefixMatch(val: string): (v: MessageChain) => boolean { 122 | return (v: MessageChain): boolean => chain2str(v).startsWith(val) 123 | } 124 | /** 125 | * 文本后缀匹配。 126 | * @param val 内容。 127 | * @returns 装饰器 128 | */ 129 | export function suffixMatch(val: string): (v: MessageChain) => boolean { 130 | return (v: MessageChain): boolean => chain2str(v).endsWith(val) 131 | } 132 | /** 133 | * 内容完全匹配。 134 | * @param val 正则表达式对象。 135 | * @returns 装饰器 136 | */ 137 | export function contentMatch(val: MessageBase[]): (v: MessageChain) => boolean { 138 | return (v: MessageChain): boolean => v.slice(1) == val 139 | } 140 | /** 141 | * 消息格式匹配。 142 | * @param type 143 | * @returns 装饰器 144 | */ 145 | export function templateMatch( 146 | type: MessageType[] 147 | ): (v: MessageChain) => boolean { 148 | return (v: MessageChain): boolean => 149 | v.length - 1 == type.length && 150 | v 151 | .slice(1) 152 | .every( 153 | (value: MessageBase, index: number): boolean => 154 | value.type == type[index] 155 | ) 156 | } 157 | /** 158 | * 文字正则匹配。 159 | * @param reg 正则表达式对象。 160 | * @returns this 161 | */ 162 | export function regexMatch(reg: RegExp): (v: MessageChain) => boolean { 163 | return (v: MessageChain): boolean => chain2str(v).match(reg) != undefined 164 | } 165 | /** 166 | * 是否提到某人。 167 | * @param qq 这个人的qq号。 168 | * @returns this 169 | */ 170 | export function mention(qq: UserID): (v: MessageChain) => boolean { 171 | return (v: MessageChain): boolean => 172 | !v 173 | .slice(1) 174 | .every((x: MessageBase) => !(x.type == 'At' && (x as At).target == qq)) 175 | } 176 | /** 177 | * 消息解析处理器 178 | */ 179 | class Parser { 180 | private fn: ((data: MessageChain) => MessageChain)[] = [] 181 | next(fn: ((data: MessageChain) => MessageChain)[]): this { 182 | this.fn.push(...fn) 183 | return this 184 | } 185 | done(fn: OneProcessor): AllProcessor { 186 | return async (data: AllMessage): Promise => { 187 | let msgchain = data.messageChain 188 | this.fn.forEach(fn => (msgchain = fn(msgchain))) 189 | data.messageChain = msgchain 190 | await fn(data as OneMessage) 191 | } 192 | } 193 | } 194 | /** 195 | * 消息解析处理器。 196 | * 注意:这个处理器无需生成,请直接使用。 197 | */ 198 | export const parseCmd = (cmd: MessageChain): MessageChain => { 199 | const f: MessageChain = [cmd[0]] 200 | for (let i = 1; i < cmd.length; i++) { 201 | if (cmd[i].type == 'Plain') { 202 | const s = cmd[i] as Plain 203 | let temp = '' 204 | for (let j = 0; j < s.text.length; j++) { 205 | if (s.text[j] == ' ') { 206 | if (temp != '') f.push(new Plain(temp)) 207 | temp = '' 208 | } else temp += s.text[j] 209 | } 210 | if (temp != '') f.push(new Plain(temp)) 211 | } else { 212 | f.push(cmd[i]) 213 | } 214 | } 215 | return f 216 | } 217 | class MiddlewareBase { 218 | private _matcher: Matcher 219 | private _parser: Parser 220 | /** 221 | * 设定匹配装饰器。 222 | * @param v 匹配装饰器。 223 | */ 224 | matcher(v: ((v: MessageChain) => boolean)[]): this { 225 | this._matcher.next(v) 226 | return this 227 | } 228 | /** 229 | * 启用命令解释器 230 | * @param v 命令解释器。 231 | * @returns this 232 | */ 233 | parser(v: ((data: MessageChain) => MessageChain)[]): this { 234 | this._parser.next(v) 235 | return this 236 | } 237 | static done(base: MiddlewareBase, fn: OneProcessor): AllProcessor { 238 | return base._parser.done( 239 | base._matcher.done(async (data: AllMessage): Promise => { 240 | await fn(data as OneMessage) 241 | }) 242 | ) 243 | } 244 | constructor(n?: MiddlewareBase) { 245 | if (n) [this._matcher, this._parser] = [n._matcher, n._parser] 246 | else [this._matcher, this._parser] = [new Matcher(), new Parser()] 247 | } 248 | } 249 | /** 250 | * 群聊匹配中间件。 251 | */ 252 | class GroupMiddleware extends MiddlewareBase { 253 | private _filter: GroupFilter = new GroupFilter() 254 | /** 255 | * 设定过滤装饰器 256 | * @param v 装饰器 257 | * @returns this 258 | */ 259 | filter(v: ((data: GroupMessage) => boolean)[]): this { 260 | this._filter.next(v) 261 | return this 262 | } 263 | /** 264 | * 生成事件处理器 265 | * @param fn 处理结束后要执行的函数 266 | * @returns 事件处理器 267 | */ 268 | done(fn: Processor<'GroupMessage'>): Processor<'GroupMessage'> { 269 | return MiddlewareBase.done(this, this._filter.done(fn)) 270 | } 271 | constructor(base: MiddlewareBase, filter: GroupFilter) { 272 | super(base) 273 | this._filter = filter 274 | } 275 | } 276 | class UserMiddleware extends MiddlewareBase { 277 | private _filter: UserFilter = new UserFilter() 278 | /** 279 | * 设定过滤装饰器 280 | * @param v 装饰器 281 | * @returns this 282 | */ 283 | filter(v: ((data: FriendMessage) => boolean)[]): this { 284 | this._filter.next(v) 285 | return this 286 | } 287 | /** 288 | * 生成事件处理器 289 | * @param fn 处理结束后要执行的函数 290 | * @returns 事件处理器 291 | */ 292 | done(fn: Processor<'FriendMessage'>): Processor<'FriendMessage'> { 293 | return MiddlewareBase.done(this, this._filter.done(fn)) 294 | } 295 | constructor(base: MiddlewareBase, filter: UserFilter) { 296 | super(base) 297 | this._filter = filter 298 | } 299 | } 300 | /** 301 | * 生成用于用户消息的中间件。 302 | * @param option 选项。 303 | * @param option.filter 用户过滤器列表。 304 | * @param option.parser 解析器列表。 305 | * @param option.matcher 匹配器列表。 306 | */ 307 | export function Middleware({ 308 | matcher, 309 | parser, 310 | filter 311 | }: { 312 | matcher?: ((v: MessageChain) => boolean)[] 313 | parser?: ((data: MessageChain) => MessageChain)[] 314 | filter: ['user', ((data: FriendMessage) => boolean)[]] 315 | }): (fn: Processor<'FriendMessage'>) => Processor<'FriendMessage'> 316 | /** 317 | * 生成用于群组消息的中间件。 318 | * @param option 选项。 319 | * @param option.filter 群组过滤器列表。 320 | * @param option.parser 解析器列表。 321 | * @param option.matcher 匹配器列表。 322 | */ 323 | export function Middleware({ 324 | matcher, 325 | parser, 326 | filter 327 | }: { 328 | matcher?: ((v: MessageChain) => boolean)[] 329 | parser?: ((data: MessageChain) => MessageChain)[] 330 | filter: ['group', ((data: GroupMessage) => boolean)[]] 331 | }): (fn: Processor<'GroupMessage'>) => Processor<'GroupMessage'> 332 | /** 333 | * 生成用于通用消息的中间件。 334 | * @param option 选项。 335 | * @param option.parser 解析器列表。 336 | * @param option.matcher 匹配器列表。 337 | */ 338 | export function Middleware< 339 | T extends 'FriendMessage' | 'GroupMessage' | 'OtherClientMessage' 340 | >({ 341 | matcher, 342 | parser 343 | }: { 344 | matcher?: ((v: MessageChain) => boolean)[] 345 | parser?: ((data: MessageChain) => MessageChain)[] 346 | }): (fn: Processor) => Processor 347 | export function Middleware({ 348 | matcher = [], 349 | parser = [], 350 | filter 351 | }: { 352 | matcher?: ((v: MessageChain) => boolean)[] 353 | parser?: ((data: MessageChain) => MessageChain)[] 354 | filter?: [ 355 | 'user' | 'group', 356 | ((data: FriendMessage) => boolean)[] | ((data: GroupMessage) => boolean)[] 357 | ] 358 | }): (fn: OneProcessor) => AllProcessor { 359 | if (filter) { 360 | if (filter[0] == 'user') { 361 | const s = new UserMiddleware(new MiddlewareBase(),new UserFilter()) 362 | .filter(filter[1] as ((data: FriendMessage) => boolean)[]) 363 | .parser(parser) 364 | .matcher(matcher) 365 | return s 366 | .done.bind(s) as (fn: OneProcessor) => AllProcessor 367 | } else { 368 | const s = new GroupMiddleware(new MiddlewareBase(),new GroupFilter()) 369 | .filter(filter[1] as ((data: GroupMessage) => boolean)[]) 370 | .parser(parser) 371 | .matcher(matcher) 372 | return s 373 | .done.bind(s) as (fn: OneProcessor) => AllProcessor 374 | } 375 | } else { 376 | const s = new MiddlewareBase().parser(parser).matcher(matcher) 377 | return MiddlewareBase.done.bind(null,s) 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/Event.ts: -------------------------------------------------------------------------------- 1 | import { ClientRequest, IncomingMessage } from 'http' 2 | import { User, Member, Group, OtherClient, Permission, GroupID } from './Base' 3 | import { UserID } from './Base' 4 | import { MessageChain } from './Message' 5 | /** 基本事件 */ 6 | export interface EventBase { 7 | /** 类型 */ 8 | readonly type: EventType 9 | } 10 | /** bot上线事件 */ 11 | export interface BotOnlineEvent extends EventBase { 12 | readonly type: 'BotOnlineEvent' 13 | /** 上线的bot QQ */ 14 | qq: UserID 15 | } 16 | /** bot主动下线事件 */ 17 | export interface BotOfflineEventActive extends EventBase { 18 | readonly type: 'BotOfflineEventActive' 19 | /** 下线的bot QQ */ 20 | qq: UserID 21 | } 22 | /** bot被踢下线事件 */ 23 | export interface BotOfflineEventForce extends EventBase { 24 | readonly type: 'BotOfflineEventForce' 25 | /** 下线的bot QQ */ 26 | qq: UserID 27 | } 28 | /** bot和服务器断连事件 */ 29 | export interface BotOfflineEventDropped extends EventBase { 30 | readonly type: 'BotOfflineEventDropped' 31 | /** 下线的bot QQ */ 32 | qq: UserID 33 | } 34 | /** bot重登事件 */ 35 | export interface BotReloginEvent extends EventBase { 36 | readonly type: 'BotReloginEvent' 37 | /** 重登的bot QQ */ 38 | qq: UserID 39 | } 40 | /** 好友输入状态改变事件 */ 41 | export interface FriendInputStatusChangedEvent extends EventBase { 42 | readonly type: 'FriendInputStatusChangedEvent' 43 | /** 现在的输入状态 */ 44 | inputting: boolean 45 | /** 好友信息 */ 46 | friend: User 47 | } 48 | /** 好友昵称改变事件 */ 49 | export interface FriendNickChangedEvent extends EventBase { 50 | readonly type: 'FriendNickChangedEvent' 51 | /** 好友信息 */ 52 | friend: User 53 | /** 原来的昵称 */ 54 | from: string 55 | /** 现在的昵称 */ 56 | to: string 57 | } 58 | /** Bot 群权限被改变事件 */ 59 | export interface BotGroupPermissionChangeEvent extends EventBase { 60 | readonly type: 'BotGroupPermissionChangeEvent' 61 | /** 原来的权限 */ 62 | origin: Permission 63 | /** 现在的权限 */ 64 | current: Permission 65 | /** 群聊信息 */ 66 | group: Group 67 | } 68 | /** Bot 被禁言事件 */ 69 | export interface BotMuteEvent extends EventBase { 70 | readonly type: 'BotMuteEvent' 71 | /** 时长 */ 72 | durationSeconds: number 73 | /** 操作者 */ 74 | operator: Member 75 | } 76 | /** Bot 被解禁事件 */ 77 | export interface BotUnmuteEvent extends EventBase { 78 | readonly type: 'BotUnmuteEvent' 79 | /** 操作者 */ 80 | operator: Member 81 | } 82 | /** Bot 加入群组事件 */ 83 | export interface BotJoinGroupEvent extends EventBase { 84 | readonly type: 'BotJoinGroupEvent' 85 | /** 加入的群聊 */ 86 | group: Group 87 | /** 邀请者(如果有) */ 88 | invitor?: Member 89 | } 90 | /** Bot 主动退群事件 */ 91 | export interface BotLeaveEventActive extends EventBase { 92 | readonly type: 'BotLeaveEventActive' 93 | /** 退出的群聊 */ 94 | group: Group 95 | } 96 | /** Bot 被踢事件 */ 97 | export interface BotLeaveEventKick extends EventBase { 98 | readonly type: 'BotLeaveEventKick' 99 | /** 被踢出的群聊 */ 100 | group: Group 101 | /** 操作者 */ 102 | operator: Member 103 | } 104 | /** Bot 群聊解散事件 */ 105 | export interface BotLeaveEventDisband extends EventBase { 106 | readonly type: 'BotLeaveEventDisband' 107 | /** 解散的群聊 */ 108 | group: Group 109 | /** 操作者(一定是群主) */ 110 | operator: Member 111 | } 112 | /** 群撤回消息事件 */ 113 | export interface GroupRecallEvent extends EventBase { 114 | readonly type: 'GroupRecallEvent' 115 | /** 原消息用户ID */ 116 | authorId: UserID 117 | /** 撤回消息的messageId */ 118 | messageId: number 119 | /** 原消息发送时间 */ 120 | time: number 121 | /** 群聊 */ 122 | group: Group 123 | /** 操作者(当没有数据时就是bot干的) */ 124 | operator?: Member 125 | } 126 | /** 好友撤回消息事件 */ 127 | export interface FriendRecallEvent extends EventBase { 128 | readonly type: 'FriendRecallEvent' 129 | /** 原消息用户ID */ 130 | authorId: UserID 131 | /** 原消息messageId */ 132 | messageId: number 133 | /** 原消息发送时间 */ 134 | time: number 135 | /** 好友QQ或者Bot QQ */ 136 | operator: UserID 137 | } 138 | /** 戳一戳事件 */ 139 | export interface NudgeEvent extends EventBase { 140 | readonly type: 'NudgeEvent' 141 | /** 发送者ID */ 142 | fromId: UserID 143 | subject: { 144 | /** 群号或者好友qq */ 145 | id: GroupID | UserID 146 | /** id类型 */ 147 | kind: 'Group' | 'Friend' 148 | } 149 | /** 动作类型 */ 150 | action: string 151 | /** 动作后缀 */ 152 | suffix: string 153 | /** 被戳人的QQ */ 154 | target: UserID 155 | } 156 | /** 群名变更事件 */ 157 | export interface GroupNameChangeEvent extends EventBase { 158 | readonly type: 'GroupNameChangeEvent' 159 | /** 原来的群名 */ 160 | origin: string 161 | /** 现在的群名 */ 162 | current: string 163 | /** 群号 */ 164 | group: Group 165 | /** 操作者,没有数据就是Bot干的 */ 166 | operator?: Member 167 | } 168 | /** 入群公告变更事件 */ 169 | export interface GroupEntranceAnnouncementChangeEvent extends EventBase { 170 | readonly type: 'GroupEntranceAnnouncementChangeEvent' 171 | /** 原来的入群公告 */ 172 | origin: string 173 | /** 现在的入群公告 */ 174 | current: string 175 | /** 群聊 */ 176 | group: Group 177 | /** 操作者,没有数据就是Bot干的 */ 178 | operator?: Member 179 | } 180 | /** 全员禁言事件 */ 181 | export interface GroupMuteAllEvent extends EventBase { 182 | readonly type: 'GroupMuteAllEvent' 183 | /** 原状态 */ 184 | origin: boolean 185 | /** 现在的状态 */ 186 | current: boolean 187 | /** 群聊 */ 188 | group: Group 189 | /** 操作者,没有数据就是Bot干的 */ 190 | operator?: Member 191 | } 192 | /** 群聊允许匿名聊天事件 */ 193 | export interface GroupAllowAnonymousChatEvent extends EventBase { 194 | readonly type: 'GroupAllowAnonymousChatEvent' 195 | /** 原状态 */ 196 | origin: boolean 197 | /** 现在的状态 */ 198 | current: boolean 199 | /** 群聊 */ 200 | group: Group 201 | /** 操作者,没有数据就是Bot干的 */ 202 | operator?: Member 203 | } 204 | /** 群聊允许坦白说事件 */ 205 | export interface GroupAllowConfessTalkEvent extends EventBase { 206 | readonly type: 'GroupAllowConfessTalkEvent' 207 | /** 原状态 */ 208 | origin: boolean 209 | /** 现在的状态 */ 210 | current: boolean 211 | /** 群聊 */ 212 | group: Group 213 | /** 是不是Bot干的 */ 214 | isByBot: boolean 215 | } 216 | /** 群聊允许成员邀请加群事件 */ 217 | export interface GroupAllowMemberInviteEvent extends EventBase { 218 | readonly type: 'GroupAllowMemberInviteEvent' 219 | /** 原状态 */ 220 | origin: boolean 221 | /** 现在的状态 */ 222 | current: boolean 223 | /** 群聊 */ 224 | group: Group 225 | /** 操作者,没有数据就是Bot干的 */ 226 | operator?: Member 227 | } 228 | /** 成员加入事件 */ 229 | export interface MemberJoinEvent extends EventBase { 230 | readonly type: 'MemberJoinEvent' 231 | /** 加入的成员 */ 232 | member: Member 233 | /** 邀请者(如果有) */ 234 | invitor?: Member 235 | } 236 | /** 成员被踢事件 */ 237 | export interface MemberLeaveEventKick extends EventBase { 238 | readonly type: 'MemberLeaveEventKick' 239 | /** 被踢的成员 */ 240 | member: Member 241 | /** 邀请者(如果有) */ 242 | operator?: Member 243 | } 244 | /** 成员退群事件 */ 245 | export interface MemberLeaveEventQuit extends EventBase { 246 | readonly type: 'MemberLeaveEventQuit' 247 | /** 退群的成员 */ 248 | member: Member 249 | } 250 | /** 成员群名片修改事件 */ 251 | export interface MemberCardChangeEvent extends EventBase { 252 | readonly type: 'MemberCardChangeEvent' 253 | /** 原来的群名片 */ 254 | origin: string 255 | /** 现在的群名片 */ 256 | current: string 257 | /** (被)改群名片的成员 */ 258 | member: Member 259 | } 260 | /** 成员群头衔更改事件 */ 261 | export interface MemberSpecialTitleChangeEvent extends EventBase { 262 | readonly type: 'MemberSpecialTitleChangeEvent' 263 | /** 原来的头衔 */ 264 | origin: string 265 | /** 现在的头衔 */ 266 | current: string 267 | /** 被改的成员 */ 268 | member: Member 269 | } 270 | /** 群成员权限被改事件 */ 271 | export interface MemberPermissionChangeEvent extends EventBase { 272 | readonly type: 'MemberPermissionChangeEvent' 273 | /** 原来的权限 */ 274 | origin: Permission 275 | /** 现在的权限 */ 276 | current: Permission 277 | /** 被改的成员 */ 278 | member: Member 279 | } 280 | /** 成员被禁言事件 */ 281 | export interface MemberMuteEvent extends EventBase { 282 | readonly type: 'MemberMuteEvent' 283 | /** 被禁言时长 */ 284 | durationSeconds: number 285 | /** 被禁言的成员 */ 286 | member: Member 287 | /** 操作者,没有数据就是Bot干的 */ 288 | operator?: Member 289 | } 290 | /** 成员被解禁事件 */ 291 | export interface MemberUnmuteEvent extends EventBase { 292 | readonly type: 'MemberUnmuteEvent' 293 | /** 被解禁的成员 */ 294 | member: Member 295 | /** 操作者,没有数据就是Bot干的 */ 296 | operator?: Member 297 | } 298 | /** 成员群荣誉变更事件(不是群头衔) */ 299 | export interface MemberHonorChangeEvent extends EventBase { 300 | readonly type: 'MemberHonorChangeEvent' 301 | /** 荣誉 */ 302 | honor: string 303 | /** 获得/失去 */ 304 | action: 'achieve' | 'lost' 305 | /** 变更的成员 */ 306 | member: Member 307 | } 308 | /** 用户请求添加Bot事件 */ 309 | export interface NewFriendRequestEvent extends EventBase { 310 | readonly type: 'NewFriendRequestEvent' 311 | /** 用于回复请求的ID */ 312 | eventId: number 313 | /** 谁要加Bot */ 314 | fromId: UserID 315 | /** 哪个群过来的(如果是其它方式搜索到Bot则是0) */ 316 | groupId: 0 | GroupID 317 | /** 用户昵称 */ 318 | nick: string 319 | /** 留言 */ 320 | message: string 321 | } 322 | /** 用户请求加入群事件 */ 323 | export interface MemberJoinRequestEvent extends EventBase { 324 | readonly type: 'MemberJoinRequestEvent' 325 | /** 用于回复请求的ID */ 326 | eventId: number 327 | /** 谁要进群 */ 328 | fromId: UserID 329 | /** 进哪个群 */ 330 | groupId: GroupID 331 | /** 群名字 */ 332 | groupName: string 333 | /** 用户昵称 */ 334 | nick: string 335 | /** 验证信息 */ 336 | message: string 337 | /** 邀请人(可空) @since 2.7.0 */ 338 | invitor?: number 339 | } 340 | /** Bot被邀请加群事件 */ 341 | export interface BotInvitedJoinGroupRequestEvent extends EventBase { 342 | readonly type: 'BotInvitedJoinGroupRequestEvent' 343 | /** 用于回复请求的ID */ 344 | eventId: number 345 | /** 谁在拉Bot */ 346 | fromId: UserID 347 | /** 拉到哪个群 */ 348 | groupId: GroupID 349 | /** 群名字 */ 350 | groupName: string 351 | /** 好友昵称 */ 352 | nick: string 353 | /** 留言 */ 354 | message: string 355 | } 356 | /** 其它客户端上线事件 */ 357 | export interface OtherClientOnlineEvent extends EventBase { 358 | readonly type: 'OtherClientOnlineEvent' 359 | /** 哪个客户端 */ 360 | client: OtherClient 361 | /** 客户端类型 */ 362 | kind?: number 363 | } 364 | /** 其它客户端下线事件 */ 365 | export interface OtherClientOfflineEvent extends EventBase { 366 | readonly type: 'OtherClientOfflineEvent' 367 | /** 哪个客户端 */ 368 | client: OtherClient 369 | } 370 | /** 好友消息 */ 371 | export interface FriendMessage extends EventBase { 372 | readonly type: 'FriendMessage' 373 | /** 消息链 */ 374 | messageChain: MessageChain 375 | /** 发给Bot消息的好友 */ 376 | sender: User 377 | } 378 | /** 群组消息 */ 379 | export interface GroupMessage extends EventBase { 380 | readonly type: 'GroupMessage' 381 | /** 消息链 */ 382 | messageChain: MessageChain 383 | /** 发消息的成员 */ 384 | sender: Member 385 | } 386 | /** 临时消息 */ 387 | export interface TempMessage extends EventBase { 388 | readonly type: 'TempMessage' 389 | /** 消息链 */ 390 | messageChain: MessageChain 391 | /** 发给Bot消息的成员 */ 392 | sender: Member 393 | } 394 | export interface StrangerMessage extends EventBase { 395 | readonly type: 'StrangerMessage' 396 | /** 消息链 */ 397 | messageChain: MessageChain 398 | /** 发给Bot消息的用户 */ 399 | sender: User 400 | } 401 | /** 其它客户端消息基类 */ 402 | export interface OtherClientMessage extends EventBase { 403 | readonly type: 'OtherClientMessage' 404 | /** 消息链 */ 405 | messageChain: MessageChain 406 | /** 发给Bot消息的客户端 */ 407 | sender: OtherClient 408 | } 409 | /** websocket 错误事件 */ 410 | export interface WsError extends EventBase { 411 | readonly type: 'error' 412 | /** 错误信息 */ 413 | error: Error 414 | } 415 | /** websocket 关闭事件 */ 416 | export interface WsClose extends EventBase { 417 | readonly type: 'close' 418 | code: number 419 | reason: Buffer 420 | } 421 | /** websocket 未预期的消息(报文格式不对)事件 */ 422 | export interface WsUnexpectedResponse extends EventBase { 423 | readonly type: 'unexpected-response' 424 | request: ClientRequest 425 | response: IncomingMessage 426 | } 427 | /** @private 事件id对参数类型,用于EventArg等 */ 428 | interface Events { 429 | // WebSocket 事件 430 | error: WsError 431 | close: WsClose // done 432 | 'unexpected-response': WsUnexpectedResponse // done 433 | // mirai 事件 434 | GroupMessage: GroupMessage // done 435 | FriendMessage: FriendMessage // done 436 | TempMessage: TempMessage // done 437 | StrangerMessage: StrangerMessage // done 438 | OtherClientMessage: OtherClientMessage // done 439 | BotOnlineEvent: BotOnlineEvent 440 | BotOfflineEventActive: BotOfflineEventActive 441 | BotOfflineEventForce: BotOfflineEventForce 442 | BotOfflineEventDropped: BotOfflineEventDropped 443 | BotReloginEvent: BotReloginEvent 444 | FriendInputStatusChangedEvent: FriendInputStatusChangedEvent 445 | FriendNickChangedEvent: FriendNickChangedEvent 446 | BotGroupPermissionChangeEvent: BotGroupPermissionChangeEvent 447 | BotMuteEvent: BotMuteEvent 448 | BotUnmuteEvent: BotUnmuteEvent 449 | BotJoinGroupEvent: BotJoinGroupEvent 450 | BotLeaveEventActive: BotLeaveEventActive 451 | BotLeaveEventKick: BotLeaveEventKick 452 | BotLeaveEventDisband: BotLeaveEventDisband 453 | GroupRecallEvent: GroupRecallEvent 454 | FriendRecallEvent: FriendRecallEvent 455 | NudgeEvent: NudgeEvent 456 | GroupNameChangeEvent: GroupNameChangeEvent 457 | GroupEntranceAnnouncementChangeEvent: GroupEntranceAnnouncementChangeEvent 458 | GroupMuteAllEvent: GroupMuteAllEvent 459 | GroupAllowAnonymousChatEvent: GroupAllowAnonymousChatEvent 460 | GroupAllowConfessTalkEvent: GroupAllowConfessTalkEvent 461 | GroupAllowMemberInviteEvent: GroupAllowMemberInviteEvent 462 | MemberJoinEvent: MemberJoinEvent 463 | MemberLeaveEventKick: MemberLeaveEventKick 464 | MemberLeaveEventQuit: MemberLeaveEventQuit 465 | MemberCardChangeEvent: MemberCardChangeEvent 466 | MemberSpecialTitleChangeEvent: MemberSpecialTitleChangeEvent 467 | MemberPermissionChangeEvent: MemberPermissionChangeEvent 468 | MemberMuteEvent: MemberMuteEvent 469 | MemberUnmuteEvent: MemberUnmuteEvent 470 | MemberHonorChangeEvent: MemberHonorChangeEvent 471 | NewFriendRequestEvent: NewFriendRequestEvent 472 | MemberJoinRequestEvent: MemberJoinRequestEvent 473 | BotInvitedJoinGroupRequestEvent: BotInvitedJoinGroupRequestEvent 474 | OtherClientOnlineEvent: OtherClientOnlineEvent 475 | OtherClientOfflineEvent: OtherClientOfflineEvent 476 | } 477 | /** Event类型 */ 478 | export type EventType = keyof Events 479 | /** 通用消息类型 */ 480 | export type Message = 481 | | FriendMessage 482 | | GroupMessage 483 | | OtherClientMessage 484 | | StrangerMessage 485 | | TempMessage 486 | /** 487 | * Typescript Helper:获得事件的参数类型。 488 | * 例:EventArg<'FriendMessage'> = FriendMessage 489 | * EventArg 可获得所有参数的联合类型。 490 | */ 491 | export type EventArg = Events[T] 492 | -------------------------------------------------------------------------------- /src/Bot.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket } from 'ws' 2 | import _auth from './core/auth' 3 | import _bind from './core/bind' 4 | import _stopListen from './core/listenStop' 5 | import _startListen from './core/listenBegin' 6 | import _releaseSession from './core/release' 7 | import _sendTempMessage from './core/sendTempMsg' 8 | import _sendFriendMessage from './core/sendFriendMsg' 9 | import _sendGroupMessage from './core/sendGroupMsg' 10 | import _sendNudge from './core/sendNudge' 11 | import _recall from './core/recallMsg' 12 | import _uploadImage from './core/uploadImage' 13 | import _uploadVoice from './core/uploadVoice' 14 | import _getFriendList from './core/getFriendList' 15 | import _getGroupList from './core/getGroupList' 16 | import _getMemberList from './core/getMemberList' 17 | import _getMemberInfo from './core/getMemberInfo' 18 | import _getMemberProfile from './core/getMemberProfile' 19 | import _getBotProfile from './core/getBotProfile' 20 | import _getFriendProfile from './core/getFriendProfile' 21 | import _getUserProfile from './core/getUserProfile' 22 | import _setMemberInfo from './core/setMemberInfo' 23 | import _setMemberPerm from './core/setMemberPerm' 24 | import _getAnnoList from './core/anno/getAnnoList' 25 | import _publishAnno from './core/anno/publishAnno' 26 | import _deleteAnno from './core/anno/deleteAnno' 27 | import _mute from './core/mute' 28 | import _muteAll from './core/muteAll' 29 | import _unmute from './core/unmute' 30 | import _unmuteAll from './core/unmuteAll' 31 | import _removeMember from './core/removeMember' 32 | import _removeFriend from './core/removeFriend' 33 | import _quitGroup from './core/quitGroup' 34 | import _getGroupConfig from './core/getGroupConfig' 35 | import _setGroupConfig from './core/setGroupConfig' 36 | import _botInvited from './core/event/botInvited' 37 | import _memberJoin from './core/event/memberJoin' 38 | import _newFriend from './core/event/newFriend' 39 | import _setEssence from './core/setEssence' 40 | import _getMsg from './core/getMsg' 41 | import { 42 | EventType, 43 | NewFriendRequestEvent, 44 | MemberJoinRequestEvent, 45 | BotInvitedJoinGroupRequestEvent, 46 | EventArg, 47 | EventBase, 48 | GroupMessage, 49 | Message, 50 | FriendMessage, 51 | StrangerMessage, 52 | TempMessage 53 | } from './Event' 54 | import { MessageBase, Source, Voice } from './Message' 55 | import { 56 | Announcement, 57 | Group, 58 | Member, 59 | Profile, 60 | User, 61 | UserID, 62 | MemberID, 63 | GroupID, 64 | GroupInfo, 65 | MemberSetting, 66 | OtherClient 67 | } from './Base' 68 | import { FileManager } from './File' 69 | /** 70 | * Typescript Helper:获得事件的处理器类型。 71 | * 例:Processor<'FriendMessage'> = (data: FriendMessage) => Promise | void 72 | */ 73 | export type Processor = ( 74 | data: EventArg 75 | ) => Promise | void 76 | /** 77 | * Typescript Helper:获得事件的匹配器类型。 78 | * 例:Matcher<'FriendMessage'> = (data: FriendMessage) => boolean 79 | */ 80 | export type Matcher = (data: EventArg) => boolean 81 | /** 82 | * 获得事件在事件池中的定位。 83 | */ 84 | export class EventIndex { 85 | type: T 86 | index: number 87 | constructor({ type, index }: { type: T; index: number }) { 88 | void ([this.type, this.index] = [type, index]) 89 | } 90 | } 91 | /** send方法要求的选项 */ 92 | interface SendOption { 93 | /** 账户 qq 号或者群员上下文(上下文用于发送临时消息) */ 94 | qq: T 95 | /** 要回复的消息,可以是Bot发送的 */ 96 | reply?: Message 97 | /** 要发送的消息 */ 98 | message: MessageBase[] 99 | } 100 | /** remove方法要求的选项 */ 101 | interface RemoveOption { 102 | /** 账户或群聊 qq 号或者群员 */ 103 | qq: T 104 | /** 留言 */ 105 | message?: string 106 | } 107 | /** upload方法要求的选项 */ 108 | interface UploadOption { 109 | /** 要上传的东西 */ 110 | data: Buffer 111 | /** 后辍名 */ 112 | suffix: string 113 | } 114 | interface TemplateImage { 115 | imageId: string 116 | url: string 117 | path: '' 118 | } 119 | /** anno publish方法要求的选项 */ 120 | interface AnnoOption { 121 | /** 公告内容 */ 122 | content: string 123 | /** 是否置顶 */ 124 | pinned?: boolean 125 | } 126 | /** 用户提供的机器人配置 */ 127 | interface Config { 128 | /** 账户QQ号。 */ 129 | qq: UserID 130 | /** 认证密码。 */ 131 | verifyKey: string 132 | /** websocket地址。mirai-api-http API问题导致不得不同时使用http/ws。 */ 133 | wsUrl: string 134 | /** http地址。mirai-api-http API问题导致不得不同时使用http/ws。 */ 135 | httpUrl: string 136 | } 137 | /** 完整的机器人配置 */ 138 | interface FullConfig extends Config { 139 | /** 会话token。 */ 140 | sessionKey: string 141 | } 142 | export class Bot { 143 | /** @private 机器人内部使用的Config。 */ 144 | private conf?: FullConfig = undefined 145 | /** @private 机器人内部使用的Websocket连接。 */ 146 | private ws?: WebSocket = undefined 147 | /** @private 内部存储的事件池 */ 148 | private event: Partial[]>>> = 149 | {} 150 | /** @private wait函数的等待器集合 */ 151 | private waiting: Partial[]>>> = 152 | {} 153 | private static clone(data: T): T { 154 | if (typeof data !== 'object' || data == undefined) return data 155 | const result = Object.create( 156 | Object.getPrototypeOf(data), 157 | Object.getOwnPropertyDescriptors(data) 158 | ) 159 | Reflect.ownKeys(data).forEach( 160 | key => 161 | (result[key] = Bot.clone( 162 | (data as Record)[key] 163 | )) 164 | ) 165 | return result 166 | } 167 | /** 168 | * @private ws监听初始化。 169 | */ 170 | private async listen_ws(): Promise { 171 | // 判断是否有conf 172 | if (!this.conf) return 173 | // 绑定sessionKey和qq 174 | await _bind({ 175 | httpUrl: this.conf.httpUrl, 176 | sessionKey: this.conf.sessionKey, 177 | qq: this.conf.qq 178 | }) 179 | // 设定ws 180 | this.ws = await _startListen({ 181 | wsUrl: this.conf.wsUrl, 182 | sessionKey: this.conf.sessionKey, 183 | verifyKey: this.conf.verifyKey, 184 | message: (data: EventBase) => { 185 | // 收到事件 186 | this.dispatch(data as EventArg) // 强转:此处要将data转为联合类型(无法在编译期确定data类型)。 187 | }, 188 | error: err => { 189 | this.dispatch(err) 190 | console.error('ws error', err) 191 | }, 192 | close: obj => { 193 | this.dispatch(obj) 194 | console.log('ws close', obj) 195 | }, 196 | unexpectedResponse: obj => { 197 | this.dispatch(obj) 198 | console.error('ws unexpectedResponse', obj) 199 | } 200 | }) 201 | } 202 | /** 203 | * 获得设置项。 204 | */ 205 | get config(): FullConfig { 206 | if (!this.conf) throw new Error('config 请先调用 open,建立一个会话') 207 | return this.conf 208 | } 209 | /** 210 | * 由消息ID获得一条消息 211 | * @param messageId 消息ID 212 | * @param target 目标群/好友QQ号 213 | * @returns 消息 214 | */ 215 | async msg(target: GroupID | UserID, messageId: number): Promise { 216 | // 检查对象状态 217 | if (!this.conf) throw new Error('config 请先调用 open,建立一个会话') 218 | // 需要使用的参数 219 | const { httpUrl, sessionKey } = this.conf 220 | return await _getMsg({ 221 | httpUrl, 222 | sessionKey, 223 | target, 224 | messageId 225 | }) 226 | } 227 | /** 228 | * 等待由matcher匹配的指定事件。在匹配成功时不会触发其它触发器。 229 | * @param type 事件类型。 230 | * @param matcher 匹配器。当匹配器返回true时才会回传事件。 231 | * @returns 匹配到的事件。 232 | */ 233 | wait( 234 | type: T, 235 | matcher: Matcher 236 | ): Promise> { 237 | // 检查对象状态 238 | if (!this.conf) throw new Error('wait 请先调用 open,建立一个会话') 239 | return new Promise>(resolve => { 240 | const resolver = ((data: EventArg): boolean => { 241 | if (matcher(data)) { 242 | resolve(data) 243 | return true 244 | } 245 | return false 246 | }) as Matcher 247 | if (this.waiting[type]) this.waiting[type]?.push(resolver) 248 | else this.waiting[type] = [resolver] 249 | }) 250 | } 251 | /** 252 | * 关闭机器人连接。 253 | * @param option 选项。 254 | * @param option.keepProcessor 是否保留事件处理器。 255 | */ 256 | async close({ keepProcessor = false } = {}): Promise { 257 | // 检查对象状态 258 | if (!this.conf) throw new Error('close 请先调用 open,建立一个会话') 259 | // 需要使用的参数 260 | const { httpUrl, sessionKey, qq } = this.conf 261 | // 关闭 ws 连接 262 | if (this.ws) await _stopListen(this.ws) 263 | // 释放会话 264 | await _releaseSession({ httpUrl, sessionKey, qq: qq }) 265 | // 初始化对象状态 266 | if (!keepProcessor) this.off() 267 | this.conf = undefined 268 | this.ws = undefined 269 | } 270 | /** 271 | * 启动机器人连接。 272 | * @param conf 连接设定。 273 | */ 274 | async open(conf: Config): Promise { 275 | if (this.conf) this.close() 276 | this.conf = Object.assign(conf, { sessionKey: '' }) 277 | this.conf.sessionKey = await _auth({ 278 | httpUrl: this.conf.httpUrl, 279 | verifyKey: this.conf.verifyKey 280 | }) 281 | await this.listen_ws() 282 | } 283 | /** 284 | * 向 qq 好友 或 qq 群发送消息。 285 | * @param type 群聊消息|好友消息|临时消息 286 | * @param option 发送选项。 287 | * @returns 消息 288 | */ 289 | async send(type: 'group', option: SendOption): Promise 290 | async send(type: 'friend', option: SendOption): Promise 291 | async send(type: 'temp', option: SendOption): Promise 292 | async send(type: 'temp', option: SendOption): Promise 293 | async send( 294 | type: 'group' | 'friend' | 'temp', 295 | option: SendOption 296 | ): Promise { 297 | // 检查对象状态 298 | if (!this.conf) throw new Error('send 请先调用 open,建立一个会话') 299 | let msgtype: EventType, id: number, sender: User | Member | OtherClient 300 | // 需要使用的参数 301 | const { httpUrl, sessionKey } = this.conf 302 | // 根据 temp、friend、group 参数的情况依次调用 303 | if (type == 'temp') { 304 | if (typeof option.qq != 'number') { 305 | msgtype = 'TempMessage' 306 | id = await _sendTempMessage({ 307 | httpUrl, 308 | sessionKey, 309 | qq: option.qq.qq, 310 | group: option.qq.group, 311 | quote: option.reply?.messageChain[0].id, 312 | messageChain: option.message 313 | }) 314 | sender = { 315 | id: this.conf.qq, 316 | memberName: '', 317 | specialTitle: '', 318 | permission: 'MEMBER', 319 | joinTimestamp: 0, 320 | lastSpeakTimestamp: 0, 321 | muteTimeRemaining: 0, 322 | group: { 323 | id: option.qq.group, 324 | name: '', 325 | permission: 'MEMBER' 326 | } 327 | } 328 | } else { 329 | msgtype = 'StrangerMessage' 330 | id = await _sendTempMessage({ 331 | httpUrl, 332 | sessionKey, 333 | qq: option.qq, 334 | quote: option.reply?.messageChain[0].id, 335 | messageChain: option.message 336 | }) 337 | sender = { 338 | id: this.conf.qq, 339 | nickname: '', 340 | remark: '' 341 | } 342 | } 343 | } else { 344 | msgtype = type == 'friend' ? 'FriendMessage' : 'GroupMessage' 345 | id = await (type == 'friend' ? _sendFriendMessage : _sendGroupMessage)({ 346 | httpUrl, 347 | sessionKey, 348 | target: option.qq as UserID | GroupID, 349 | quote: option.reply?.messageChain[0].id, 350 | messageChain: option.message 351 | }) 352 | if (msgtype == 'FriendMessage') { 353 | sender = { 354 | id: option.qq as UserID, 355 | nickname: '', 356 | remark: '' 357 | } 358 | } else { 359 | sender = { 360 | id: this.conf.qq, 361 | memberName: '', 362 | specialTitle: '', 363 | permission: 'MEMBER', 364 | joinTimestamp: 0, 365 | lastSpeakTimestamp: 0, 366 | muteTimeRemaining: 0, 367 | group: { 368 | id: option.qq as GroupID, 369 | name: '', 370 | permission: 'MEMBER' 371 | } 372 | } 373 | } 374 | } 375 | return { 376 | type: msgtype, 377 | sender: sender as User & Member & OtherClient, 378 | messageChain: [ 379 | new Source({ 380 | id, 381 | time: Math.floor(Date.now() / 1000) 382 | }), 383 | ...option.message 384 | ] 385 | } 386 | } 387 | /** 388 | * 向好友或群成员发送戳一戳 389 | * mirai-api-http-v1.10.1 feature 390 | * @param qq 可以是好友qq号也可以是上下文 391 | */ 392 | async nudge(qq: UserID | MemberID): Promise { 393 | // 检查对象状态 394 | if (!this.conf) throw new Error('nudge 请先调用 open,建立一个会话') 395 | // 需要使用的参数l 396 | const { httpUrl, sessionKey } = this.conf 397 | if (typeof qq != 'number') { 398 | await _sendNudge({ 399 | httpUrl, 400 | sessionKey, 401 | target: qq.qq, 402 | subject: qq.group, 403 | kind: 'Friend' 404 | }) 405 | } else { 406 | await _sendNudge({ 407 | httpUrl, 408 | sessionKey, 409 | target: qq, 410 | subject: qq, 411 | kind: 'Group' 412 | }) 413 | } 414 | } 415 | /** 416 | * 触发一个事件。 417 | * @param value 事件参数。 418 | */ 419 | dispatch(value: EventArg): void { 420 | // 如果当前到达的事件拥有处理器,则依次调用所有该事件的处理器 421 | const f = this.waiting[value.type] 422 | if (f) { 423 | // 此事件已被锁定 424 | for (const v in f) { 425 | const d = f[v] 426 | if (d) { 427 | // 事件被触发 428 | if (d(Bot.clone(value))) { 429 | f[v] = undefined 430 | this.waiting[value.type] = f 431 | return 432 | } 433 | } 434 | } 435 | } 436 | this.event[value.type]?.forEach( 437 | (i?: Processor): void => void (i ? i(Bot.clone(value)) : null) 438 | ) 439 | } 440 | /** 441 | * 添加一个事件处理器 442 | * 框架维护的 WebSocket 实例会在 ws 的事件 message 下分发 Mirai http server 的消息。 443 | * @param type 事件类型 444 | * @param callback 回调函数 445 | * @returns 事件处理器的标识,用于移除该处理器 446 | */ 447 | on(type: T, callback: Processor): EventIndex { 448 | // 检查对象状态 449 | if (!this.conf) throw new Error('on 请先调用 open,建立一个会话') 450 | // 生成EventID用于标识。 451 | let t = this.event[type] 452 | if (!t) t = [] 453 | let i = t.indexOf(undefined) 454 | if (i == -1) { 455 | t.push(callback as Processor) 456 | i = t.length - 1 457 | } else { 458 | t[i] = callback as Processor 459 | } 460 | this.event[type] = t 461 | return new EventIndex({ type, index: i }) 462 | } 463 | /** 464 | * 添加一个一次性事件处理器,回调一次后自动移除 465 | * @param type 事件类型 466 | * @param callback 回调函数 467 | * @param strict 是否严格检测调用,由于消息可能会被中间件拦截 468 | * 当为 true 时,只有开发者的处理器结束后才会移除该处理器 469 | * 当为 false 时,将不等待开发者的处理器,直接移除 470 | */ 471 | one( 472 | type: T, 473 | callback: Processor, 474 | strict = false 475 | ): void { 476 | // 检查对象状态 477 | if (!this.conf) throw new Error('on 请先调用 open,建立一个会话') 478 | let index: EventIndex = new EventIndex({ type, index: 0 }) 479 | const processor: Processor = async ( 480 | data: EventArg 481 | ): Promise => { 482 | strict ? await callback(data) : callback(data) 483 | this.off(index) 484 | } 485 | index = this.on(type, processor) 486 | } 487 | /** 488 | * 移除全部处理器 489 | */ 490 | off(): void 491 | /** 492 | * 移除type下的所有处理器 493 | * @param type 事件类型 494 | */ 495 | off(type: T): void 496 | /** 497 | * 移除handle指定的事件处理器 498 | * @param handle 事件处理器标识,由 on 方法返回。 499 | */ 500 | off(handle: EventIndex): void 501 | /** 502 | * 移除多个handle指定的事件处理器 503 | * @param handle 事件处理器标识数组,由多个 on 方法的返回值拼接而成。 504 | */ 505 | off(handle: EventIndex[]): void 506 | off( 507 | option?: EventType | EventIndex | EventIndex[] 508 | ): void { 509 | // 检查对象状态 510 | if (!this.conf) throw new Error('off 请先调用 open,建立一个会话') 511 | if (option) { 512 | if (option instanceof EventIndex) { 513 | const t = this.event[option.type] 514 | if (!t) return 515 | // 从 field eventProcessorMap 中移除 handle 指定的事件处理器 516 | if (t.length > option.index) t[option.index] = undefined 517 | this.event[option.type] = t 518 | } else if (option instanceof Array) { 519 | // 可迭代 520 | option.forEach((hd: EventIndex) => this.off(hd)) 521 | } else this.event[option] = [] // 只提供type,移除所有 522 | } else this.event = {} 523 | } 524 | /** 525 | * 撤回消息 526 | * @param message 欲撤回的消息 527 | */ 528 | async recall(message: Message): Promise { 529 | // 检查对象状态 530 | if (!this.conf) throw new Error('recall 请先调用 open,建立一个会话') 531 | const { httpUrl, sessionKey } = this.conf 532 | // 撤回消息 533 | await _recall({ 534 | httpUrl, 535 | sessionKey, 536 | target: message.sender.id, 537 | messageId: message.messageChain[0].id 538 | }) 539 | } 540 | /** 541 | * FIXME: 上游错误:type 指定为 'friend' 或 'temp' 时发送的图片显示红色感叹号,无法加载,group 则正常 542 | * 上传图片至服务器,返回指定 type 的 imageId,url,及 path 543 | * @param type "friend" 或 "group" 或 "temp" 544 | * @param option 选项 545 | * @param option.data 图片二进制数据 546 | * @param option.suffix 图片文件后缀名,默认为"jpg" 547 | * @returns 擦除类型的 Image 或 FlashImage 对象,可经实际构造后插入到message中。 548 | */ 549 | async upload( 550 | type: ['image', 'friend' | 'group' | 'temp'], 551 | option: UploadOption 552 | ): Promise 553 | /** 554 | * FIXME: 上游错误:目前该功能返回的 voiceId 无法正常使用,无法发送给好友,提示 message is empty,发到群里则是 1s 的无声语音 555 | * TODO: 上游todo:目前type仅支持 "group",请一定指定为"group",否则将导致未定义行为。 556 | * 上传语音至服务器,返回 voiceId, url 及 path 557 | * @param type "friend" 或 "group" 或 "temp"。 558 | * @param option 选项 559 | * @param option.data 语音二进制数据。 560 | * @param option.suffix 语音文件后缀名,默认为"amr"。 561 | * @returns Voice 对象,可直接插入到message中。 562 | */ 563 | async upload( 564 | type: ['voice', 'friend' | 'group' | 'temp'], 565 | option: UploadOption 566 | ): Promise 567 | async upload( 568 | type: ['image' | 'voice', 'friend' | 'group' | 'temp'], 569 | option: UploadOption 570 | ): Promise { 571 | // 检查对象状态 572 | if (!this.conf) throw new Error('upload 请先调用 open,建立一个会话') 573 | const { httpUrl, sessionKey } = this.conf 574 | if (type[0] == 'image') { 575 | return await _uploadImage({ 576 | httpUrl, 577 | sessionKey, 578 | type: type[1], 579 | img: option.data, 580 | suffix: option.suffix 581 | }) 582 | } else { 583 | return await _uploadVoice({ 584 | httpUrl, 585 | sessionKey, 586 | type: type[1], 587 | voice: option.data, 588 | suffix: option.suffix 589 | }) 590 | } 591 | } 592 | /** 593 | * 列出好友。 594 | * @param type 要列出的类型。 595 | * @returns 好友列表 596 | */ 597 | async list(type: 'friend'): Promise 598 | /** 599 | * 列出群聊。 600 | * @param type 要列出的类型。 601 | * @returns 好友列表 602 | */ 603 | async list(type: 'group'): Promise 604 | /** 605 | * 列出好友。 606 | * @param type 要列出的类型。 607 | * @param id 群聊qq。 608 | * @returns 群员列表 609 | */ 610 | async list(type: 'member', id: GroupID): Promise 611 | async list( 612 | type: 'friend' | 'group' | 'member', 613 | id?: GroupID 614 | ): Promise { 615 | if (!this.conf) throw new Error('list 请先调用 open,建立一个会话') 616 | const { httpUrl, sessionKey } = this.conf 617 | if (type == 'member') { 618 | if (!id) throw new Error('list 必须指定id') 619 | return await _getMemberList({ 620 | httpUrl, 621 | sessionKey, 622 | target: id 623 | }) 624 | } else { 625 | if (id) throw new Error('list 不能指定id') 626 | return await (type == 'friend' ? _getFriendList : _getGroupList)({ 627 | httpUrl, 628 | sessionKey 629 | }) 630 | } 631 | } 632 | /** 633 | * 获取群成员设置 634 | * @param info 上下文对象。 635 | * @returns 群成员设置 636 | */ 637 | async get(info: MemberID): Promise 638 | /** 639 | * 获取群信息 640 | * @param info 群号 641 | * @returns 群信息 642 | */ 643 | async get(info: GroupID): Promise 644 | async get(info: MemberID | GroupID): Promise { 645 | // 检查对象状态 646 | if (!this.conf) throw new Error('getMember 请先调用 open,建立一个会话') 647 | // 获取列表 648 | const { httpUrl, sessionKey } = this.conf 649 | if (typeof info != 'number') { 650 | return await _getMemberInfo({ 651 | httpUrl, 652 | sessionKey, 653 | target: info.group, 654 | memberId: info.qq 655 | }) 656 | } else { 657 | return await _getGroupConfig({ httpUrl, sessionKey, target: info }) 658 | } 659 | } 660 | /** 661 | * 设定群成员设置 662 | * @param info 上下文对象。 663 | * @param setting 成员设定。 664 | * @returns 群成员设置 665 | */ 666 | async set( 667 | info: MemberID, 668 | setting: { 669 | memberName?: string 670 | specialTitle?: string 671 | permission?: 'ADMINISTRATOR' | 'MEMBER' 672 | } 673 | ): Promise 674 | /** 675 | * 设定群设置 676 | * @param info 上下文对象。 677 | * @param setting 群设定。 678 | * @returns 群成员设置 679 | */ 680 | async set(info: GroupID, setting: GroupInfo): Promise 681 | async set( 682 | info: GroupID | MemberID, 683 | setting: MemberSetting | GroupInfo 684 | ): Promise { 685 | // 检查对象状态 686 | if (!this.conf) throw new Error('getMember 请先调用 open,建立一个会话') 687 | // 获取列表 688 | const { httpUrl, sessionKey } = this.conf 689 | if (typeof info != 'number') { 690 | const v = setting as MemberSetting 691 | await _setMemberInfo({ 692 | httpUrl, 693 | sessionKey, 694 | target: info.group, 695 | memberId: info.qq, 696 | info: { 697 | name: v.memberName, 698 | specialTitle: v.specialTitle 699 | } 700 | }) 701 | // setPermission 702 | if (v.permission) { 703 | await _setMemberPerm({ 704 | httpUrl, 705 | sessionKey, 706 | target: info.group, 707 | memberId: info.qq, 708 | assign: v.permission == 'ADMINISTRATOR' 709 | }) 710 | } 711 | } else { 712 | await _setGroupConfig({ 713 | httpUrl, 714 | sessionKey, 715 | target: info, 716 | info: setting as GroupInfo 717 | }) 718 | } 719 | } 720 | /** 721 | * 获取用户信息 722 | * @param type 可以为"friend" 或 "member" 或 "user" 或 "bot"。在某些情况下friend和user可以混用,但获得信息的详细程度可能不同。 723 | * @param target 上下文。 724 | * @returns 用户资料 725 | */ 726 | async profile(type: 'friend', target: UserID): Promise 727 | async profile(type: 'member', target: MemberID): Promise 728 | async profile(type: 'user', target: UserID): Promise 729 | async profile(type: 'bot', target: void): Promise 730 | async profile( 731 | type: 'friend' | 'member' | 'user' | 'bot', 732 | target: UserID | MemberID | void 733 | ): Promise { 734 | // 检查对象状态 735 | if (!this.conf) throw new Error('profile 请先调用 open,建立一个会话') 736 | const { httpUrl, sessionKey } = this.conf 737 | // 检查参数 738 | if (type == 'member') { 739 | const v = target as MemberID 740 | return await _getMemberProfile({ 741 | httpUrl, 742 | sessionKey, 743 | target: v.group, 744 | memberId: v.qq 745 | }) 746 | } else if (type == 'bot') { 747 | return await _getBotProfile({ httpUrl, sessionKey }) 748 | } else { 749 | return await (type == 'friend' ? _getFriendProfile : _getUserProfile)({ 750 | httpUrl, 751 | sessionKey, 752 | target: target as UserID 753 | }) 754 | } 755 | } 756 | /** 757 | * 获取群公告列表 758 | * @param group 群号 759 | * @returns 群公告列表 760 | */ 761 | async anno(type: 'list', group: GroupID): Promise 762 | /** 763 | * 删除群公告 764 | * @param group 群号 765 | * @param op 公告 id 766 | */ 767 | async anno(type: 'remove', group: GroupID, op: Announcement): Promise 768 | /** 769 | * 发布群公告 770 | * @param group 群号 771 | * @param op 发布选项 772 | */ 773 | async anno(type: 'publish', group: GroupID, op: AnnoOption): Promise 774 | async anno( 775 | type: 'list' | 'remove' | 'publish', 776 | group: GroupID, 777 | op?: Announcement | AnnoOption 778 | ): Promise { 779 | // 检查对象状态 780 | if (!this.conf) throw new Error('anno 请先调用 open,建立一个会话') 781 | // 获取配置 782 | const { httpUrl, sessionKey } = this.conf 783 | if (type == 'list') { 784 | let offset = 0 785 | let temp: Announcement[] 786 | const anno: Announcement[] = [] 787 | while ( 788 | (temp = await _getAnnoList({ 789 | httpUrl, 790 | sessionKey, 791 | id: group, 792 | offset, 793 | size: 10 794 | })).length > 0 795 | ) { 796 | anno.push(...temp) 797 | // 获取下一页 798 | offset += 10 799 | } 800 | return anno 801 | } else { 802 | if (!op) throw Error('anno 需要op参数') 803 | if (type == 'publish') { 804 | const v = op as AnnoOption 805 | await _publishAnno({ 806 | httpUrl, 807 | sessionKey, 808 | target: group, 809 | content: v.content, 810 | pinned: v.pinned 811 | }) 812 | } else { 813 | const v = op as Announcement 814 | await _deleteAnno({ httpUrl, sessionKey, id: group, fid: v.fid }) 815 | } 816 | } 817 | } 818 | /** 819 | * 禁言群成员 820 | * @param qq 群员(单体禁言)或群号(全体禁言) 821 | * @param time 禁言时长,单位: s (秒) 822 | */ 823 | async mute(qq: MemberID, time: number): Promise 824 | /** 825 | * 全体禁言 826 | * @param qq 群员(单体禁言)或群号(全体禁言) 827 | */ 828 | async mute(qq: GroupID): Promise 829 | async mute(qq: MemberID | GroupID, time?: number): Promise { 830 | // 检查对象状态 831 | if (!this.conf) throw new Error('mute 请先调用 open,建立一个会话') 832 | const { httpUrl, sessionKey } = this.conf 833 | if (typeof qq != 'number') { 834 | if (!time) throw new Error('mute 必须指定时长') 835 | await _mute({ 836 | httpUrl, 837 | sessionKey, 838 | target: qq.group, 839 | memberId: qq.qq, 840 | time 841 | }) 842 | } else { 843 | await _muteAll({ httpUrl, sessionKey, target: qq }) 844 | } 845 | } 846 | /** 847 | * 解除禁言 848 | * @param qq 群员(单体解禁)或群号(全体解禁) 849 | */ 850 | async unmute(qq: MemberID | GroupID): Promise { 851 | // 检查对象状态 852 | if (!this.conf) throw new Error('unmute 请先调用 open,建立一个会话') 853 | const { httpUrl, sessionKey } = this.conf 854 | if (typeof qq != 'number') 855 | await _unmute({ httpUrl, sessionKey, target: qq.group, memberId: qq.qq }) 856 | else await _unmuteAll({ httpUrl, sessionKey, target: qq }) 857 | } 858 | /** 859 | * 移除群成员,好友或群 860 | * @param type 要移除的类型,可以是'friend'或'group'或'member' 861 | * @param option 移除选项。 862 | */ 863 | async remove(type: 'friend', option: RemoveOption): Promise 864 | async remove(type: 'group', option: RemoveOption): Promise 865 | async remove(type: 'member', option: RemoveOption): Promise 866 | async remove( 867 | type: 'friend' | 'group' | 'member', 868 | option: RemoveOption 869 | ): Promise { 870 | // 检查对象状态 871 | if (!this.conf) throw new Error('remove 请先调用 open,建立一个会话') 872 | const { httpUrl, sessionKey } = this.conf 873 | if (type == 'member') { 874 | const v = option.qq as MemberID 875 | await _removeMember({ 876 | httpUrl, 877 | sessionKey, 878 | target: v.group, 879 | memberId: v.qq, 880 | msg: option.message ?? '' 881 | }) 882 | } else { 883 | await (type == 'friend' ? _removeFriend : _quitGroup)({ 884 | httpUrl, 885 | sessionKey, 886 | target: option.qq as UserID | GroupID 887 | }) 888 | } 889 | } 890 | /** 891 | * 获得群文件管理器实例。 892 | * @param group 群号 893 | * @returns 群文件管理器 894 | */ 895 | file(group: GroupID): FileManager { 896 | if (!this.conf) throw new Error('file 请先调用 open,建立一个会话') 897 | return new FileManager(this, group) 898 | } 899 | /** 900 | * 响应新朋友事件。 901 | * @param event 事件。 902 | * @param option 选项。 903 | * @param option.action 要进行的操作。"accept":同意。"refuse":拒绝。"refusedie":拒绝并不再受理此请求。 904 | * @param option.message 附带的信息。 905 | */ 906 | async action( 907 | event: NewFriendRequestEvent, 908 | option: { 909 | action: 'accept' | 'refuse' | 'refusedie' 910 | message?: string 911 | } 912 | ): Promise 913 | /** 914 | * 响应成员加入事件。 915 | * @param event 事件。 916 | * @param option 选项。 917 | * @param option.action 要进行的操作。"accept":同意。"refuse":拒绝。"ignore":忽略。"ignoredie":忽略并不再受理此请求。"refusedie":拒绝并不再受理此请求。 918 | * @param option.message 附带的信息。 919 | */ 920 | async action( 921 | event: MemberJoinRequestEvent, 922 | option: { 923 | action: 'accept' | 'refuse' | 'ignore' | 'ignoredie' | 'refusedie' 924 | message?: string 925 | } 926 | ): Promise 927 | /** 928 | * 响应被邀请加入群事件。 929 | * @param event 事件。 930 | * @param option 选项。 931 | * @param option.action 要进行的操作。"accept":同意。"refuse":拒绝。 932 | * @param option.message 附带的信息。 933 | */ 934 | async action( 935 | event: BotInvitedJoinGroupRequestEvent, 936 | option: { 937 | action: 'accept' | 'refuse' 938 | message?: string 939 | } 940 | ): Promise 941 | async action( 942 | event: 943 | | NewFriendRequestEvent 944 | | MemberJoinRequestEvent 945 | | BotInvitedJoinGroupRequestEvent, 946 | option: { 947 | action: 'accept' | 'refuse' | 'ignore' | 'ignoredie' | 'refusedie' 948 | message?: string 949 | } 950 | ): Promise { 951 | if (!this.conf) throw new Error('action 请先调用 open,建立一个会话') 952 | const { httpUrl, sessionKey } = this.conf 953 | if (event.type == 'NewFriendRequestEvent') { 954 | if ( 955 | option.action != 'accept' && 956 | option.action != 'refuse' && 957 | option.action != 'refusedie' 958 | ) 959 | throw new Error('action 不允许此动作') 960 | await _newFriend({ 961 | httpUrl, 962 | sessionKey, 963 | event, 964 | option: { 965 | action: option.action, 966 | message: option.message 967 | } 968 | }) 969 | } else if (event.type == 'MemberJoinRequestEvent') { 970 | await _memberJoin({ 971 | httpUrl, 972 | sessionKey, 973 | event, 974 | option 975 | }) 976 | } else { 977 | if (option.action != 'accept' && option.action != 'refuse') 978 | throw new Error('action 不允许此动作') 979 | await _botInvited({ 980 | httpUrl, 981 | sessionKey, 982 | event, 983 | option: { 984 | action: option.action, 985 | message: option.message 986 | } 987 | }) 988 | } 989 | } 990 | /** 991 | * 设置群精华消息 992 | * @param message 消息 993 | */ 994 | async essence(message: GroupMessage): Promise { 995 | // 检查对象状态 996 | if (!this.conf) throw new Error('setEssence 请先调用 open,建立一个会话') 997 | const { httpUrl, sessionKey } = this.conf 998 | await _setEssence({ 999 | httpUrl, 1000 | sessionKey, 1001 | target: message.sender.group.id, 1002 | messageId: message.messageChain[0].id 1003 | }) 1004 | } 1005 | } 1006 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------