From 408f9c72c208473978d3dd07c604204bdb99a03b Mon Sep 17 00:00:00 2001 From: CakmLexi Date: Tue, 2 Jul 2024 14:34:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=88=E5=B9=B6dev=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81ts=E7=89=88=E6=9C=AC=20=E6=94=AF=E6=8C=81npm=E5=8C=85?= =?UTF-8?q?=20=E6=9A=82=E6=97=B6=E4=BF=9D=E7=95=99=E5=AF=B9=E6=97=A7?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=9A=84=E6=A8=A1=E5=9D=97=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 21 +- config/defSet/config.yaml | 29 +- eslint.config.js | 20 +- lib/adapter/adapter.js | 555 ------------ lib/adapter/kritor/api.js | 853 ------------------ lib/adapter/kritor/converter.js | 423 --------- lib/adapter/kritor/index.js | 172 ---- lib/adapter/onebot/OneBot11.js | 1461 ------------------------------- lib/bot/KarinElement.js | 920 ------------------- lib/bot/KarinEvent.js | 180 ---- lib/bot/KarinMessage.js | 144 --- lib/bot/KarinNotice.js | 1009 --------------------- lib/bot/KarinRequest.js | 215 ----- lib/bot/UserInfo.js | 26 - lib/common/common.js | 416 --------- lib/core/config.js | 355 -------- lib/core/listener.js | 247 ------ lib/core/logger.js | 125 --- lib/core/online.js | 38 - lib/core/process.js | 160 ---- lib/core/server.js | 261 ------ lib/db/level.js | 38 - lib/db/redis.js | 158 ---- lib/db/redis_level.js | 309 ------- lib/event/event.js | 193 ---- lib/event/message.js | 259 ------ lib/event/notice.js | 211 ----- lib/event/request.js | 84 -- lib/event/review.js | 388 -------- lib/index.js | 133 +-- lib/plugins/loader.js | 409 --------- lib/plugins/plugin.js | 190 ---- lib/renderer/App.js | 126 --- lib/renderer/Base.js | 99 --- lib/renderer/Client.js | 172 ---- lib/renderer/Http.js | 57 -- lib/renderer/Server.js | 129 --- lib/renderer/Wormhole.js | 166 ---- lib/tools/install.js | 180 ---- lib/tools/pm2Log.js | 14 - lib/tools/uninstall.js | 64 -- lib/tools/updateVersion.js | 5 - lib/utils/YamlEditor.js | 212 ----- lib/utils/app.js | 163 ---- lib/utils/button.js | 91 -- lib/utils/exec.js | 50 -- lib/utils/ffmpeg.js | 26 - lib/utils/handler.js | 120 --- lib/utils/protobuf.js | 282 ------ lib/utils/segment.js | 373 -------- lib/utils/update.js | 164 ---- lib/utils/updateVersion.js | 146 --- package.json | 24 +- 53 files changed, 68 insertions(+), 12597 deletions(-) delete mode 100644 lib/adapter/adapter.js delete mode 100644 lib/adapter/kritor/api.js delete mode 100644 lib/adapter/kritor/converter.js delete mode 100644 lib/adapter/kritor/index.js delete mode 100644 lib/adapter/onebot/OneBot11.js delete mode 100644 lib/bot/KarinElement.js delete mode 100644 lib/bot/KarinEvent.js delete mode 100644 lib/bot/KarinMessage.js delete mode 100644 lib/bot/KarinNotice.js delete mode 100644 lib/bot/KarinRequest.js delete mode 100644 lib/bot/UserInfo.js delete mode 100644 lib/common/common.js delete mode 100644 lib/core/config.js delete mode 100644 lib/core/listener.js delete mode 100644 lib/core/logger.js delete mode 100644 lib/core/online.js delete mode 100644 lib/core/process.js delete mode 100644 lib/core/server.js delete mode 100644 lib/db/level.js delete mode 100644 lib/db/redis.js delete mode 100644 lib/db/redis_level.js delete mode 100644 lib/event/event.js delete mode 100644 lib/event/message.js delete mode 100644 lib/event/notice.js delete mode 100644 lib/event/request.js delete mode 100644 lib/event/review.js delete mode 100644 lib/plugins/loader.js delete mode 100644 lib/plugins/plugin.js delete mode 100644 lib/renderer/App.js delete mode 100644 lib/renderer/Base.js delete mode 100644 lib/renderer/Client.js delete mode 100644 lib/renderer/Http.js delete mode 100644 lib/renderer/Server.js delete mode 100644 lib/renderer/Wormhole.js delete mode 100644 lib/tools/install.js delete mode 100644 lib/tools/pm2Log.js delete mode 100644 lib/tools/uninstall.js delete mode 100644 lib/tools/updateVersion.js delete mode 100644 lib/utils/YamlEditor.js delete mode 100644 lib/utils/app.js delete mode 100644 lib/utils/button.js delete mode 100644 lib/utils/exec.js delete mode 100644 lib/utils/ffmpeg.js delete mode 100644 lib/utils/handler.js delete mode 100644 lib/utils/protobuf.js delete mode 100644 lib/utils/segment.js delete mode 100644 lib/utils/update.js delete mode 100644 lib/utils/updateVersion.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 3ca57d1..1ee0a7d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,23 @@ "editor.detectIndentation": false, "editor.tabSize": 2, "editor.formatOnSave": true, - "javascript.format.insertSpaceBeforeFunctionParenthesis": true, - "javascript.format.insertSpaceAfterConstructor": true, + "editor.formatOnType": false, + "editor.formatOnPaste": true, + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + }, + "files.autoSave": "onFocusChange", // 单引号 "javascript.preferences.quoteStyle": "single", - // 分号 - "javascript.format.semicolons": "remove" + "typescript.preferences.quoteStyle": "single", + // 去掉分号 + "javascript.format.semicolons": "remove", + "typescript.format.semicolons": "remove", + // 函数括号前面加空格 + "javascript.format.insertSpaceBeforeFunctionParenthesis": true, + "typescript.format.insertSpaceBeforeFunctionParenthesis": true, + // 构造函数后面加空格 + "typescript.format.insertSpaceAfterConstructor": true, + "javascript.format.insertSpaceAfterConstructor": true, } \ No newline at end of file diff --git a/config/defSet/config.yaml b/config/defSet/config.yaml index b3f4fc4..92c7e30 100644 --- a/config/defSet/config.yaml +++ b/config/defSet/config.yaml @@ -1,12 +1,33 @@ -# 日志等级:trace,debug,info,warn,fatal,mark,error,off -log_level: info +# log4js 日志配置 +log4jsCfg: + # 日志等级: trace, debug, info, warn, fatal, mark, error, off + level: info + # 日志保留天数 + daysToKeep: 7 + # 整体化: 将日志输出到一个文件(一天为一个文件) 日志较多的情况下不建议与碎片化同时开启 + overall: true + # 碎片化: 将日志分片,达到指定大小后自动切割 日志较多的情况下不建议与整体化同时开启 + fragments: false + # 日志文件最大大小 MB + maxLogSize: 30 -# 日志保留天数 -log_days_Keep: 7 +# 关闭后台进程失败后是否继续启动 继续启动会导致多进程 +multi_progress: false # 控制台触发插件日志颜色 十六进制 默认#FFFF00 不支持热更新 log_color: "#E1D919" +# input适配器配置 以下所有配置均不支持热更新 +AdapterInput: + # 是否启用 + enable: true + # 是否将语音、图片、视频消息转为文件 转为文件后可通过url访问 + msgToFile: true + # url访问token 如果为 AdapterInput 每次启动后会重新生成 + token: "AdapterInput" + # 访问ip + ip: 127.0.0.1 + # ffmpeg配置 用于音视频处理 ffmpeg_path: "" ffprobe_path: "" diff --git a/eslint.config.js b/eslint.config.js index 5249929..c8d4082 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,22 +1,22 @@ -import neostandard, { resolveIgnoresFromGitignore } from 'neostandard' +import neostandard from 'neostandard' const data = neostandard({ - ignores: resolveIgnoresFromGitignore(), - globals: ['logger'], - ts: true + ignores: ['node_modules', 'temp', 'logs', 'data'], + globals: ['logger', 'NodeJS'], + ts: true, }) const newData = [] data.forEach(val => { // 驼峰命名规则关闭 - if (val.rules['camelcase']) { - val.rules['camelcase'] = ['off'] - } + if (val?.rules?.['camelcase']) val.rules['camelcase'] = ['off'] - // 排除掉plugins - if (Array.isArray(val.ignores)) { - val.ignores = val.ignores.filter((v) => !v.includes('plugins')) + // ts + if (val.name === 'neostandard/ts') { + Object.keys(val.rules).forEach((key) => { + if (val.rules[key] === 'off') val.rules[key] = 'error' + }) } newData.push(val) }) diff --git a/lib/adapter/adapter.js b/lib/adapter/adapter.js deleted file mode 100644 index dc274cb..0000000 --- a/lib/adapter/adapter.js +++ /dev/null @@ -1,555 +0,0 @@ -/** - * KarinAdapter - * - * @description KarinAdapter 适配器,所有适配器都应该继承它。用户直接看到的应该是这个继承器 - * @todo 补全方法 - * @class KarinAdapter - */ - -import { segment } from '#Karin' - -export class KarinAdapter { - constructor () { - this.index = 0 - this.self_id = '' - this.account = { uid: '', uin: '', name: '' } - this.version = { name: '', app_name: '', version: '' } - this.adapter = { id: '', name: '', type: '', sub_type: '', url: '', start_time: Date.now() } - } - - /** - * @type {number} 重连次数 - */ - index - - /** - * @type {string} 机器人id 一般情况为QQ号 - */ - self_id - - /** - * @type {account} 账号信息 - * @typedef account 账号信息 - * @property {string} account.uid - 账号uid - * @property {string} account.uin - 账号uin - * @property {string} account.name - 账号名 - */ - account - - /** - * @type {adapter} 适配器信息 - * @typedef {object} adapter 适配器信息 - * @property {'QQ' | 'WeChat' | 'Telegram' | string} adapter.id - 适配器ID - * @property {'icqq'|'onebot11'|'ontbot12'|'kritor'|string} adapter.name - 适配器名称 - * @property {'internal'|'websocket'|'grpc'|'http'|'render'} adapter.type - 适配器类型 - * @property {'server'|'client'|undefined} adapter.sub_type - 适配器子类型 - * @property {string|undefined} adapter.url - 适配器连接地址 internal和render下为undefined - * @property {number} adapter.start_time - 适配器连接时间 - */ - adapter - - /** - * @type {version} 适配器版本信息 - * @typedef {object} version 适配器版本信息 - * @property {string} version.name - 适配器名称 - * @property {string} version.app_name - 适配器名称 - * @property {string} version.version - 适配器版本 - */ - version - - /** - * 获取头像url - * @param {number} _size 头像大小,默认`0` - * @param {string|number} _uid 用户qq,默认为机器人QQ - * @returns {string} 头像的url地址 - */ - getAvatarUrl (_uid = this.self_id, _size = 0) { - throw new Error('Not implemented') - } - - /** - * 获取群头像 - * @param {string} _group_id - 群号 - * @param {number?} _size - 头像大小,默认`0` - * @param {number?} _history - 历史头像记录,默认`0`,若要获取历史群头像则填写1,2,3... - * @returns {string} - 群头像的url地址 - */ - getGroupAvatar (_group_id, _size = 0, _history = 0) { - throw new Error('Not implemented') - } - - /** - * 发送私聊消息 - * @param {number} user_id - 用户ID - * @param {Array} message - 要发送的内容 - * @returns {Promise<{message_id:string}>} - 消息ID - */ - async send_private_msg (user_id, message) { - throw new Error('Not implemented') - } - - /** - * 发送群消息 - * @param {number} group_id - 群号 - * @param {Array} message - 要发送的内容 - * @returns {Promise<{message_id:string}>} - 消息ID - */ - async send_group_msg (group_id, message) { - throw new Error('Not implemented') - } - - /** - * 发送消息 - * - * @param {KarinContact} contact - * @param {Array} elements - * @returns {Promise<{message_id:string}>} - 消息ID - */ - async SendMessage (contact, elements) { - throw new Error('Not implemented') - } - - /** - * 上传合并转发消息 - * @param {KarinContact} contact - 联系人信息 - * @param {KarinNodeElement[] | KarinNodeElement} elements - nodes - * @returns {Promise} - 资源id - * */ - async UploadForwardMessage (contact, elements) { - throw new Error('Not implemented') - } - - /** - * 撤回消息 - * @param {string?} _contact - ob11无需提供contact参数 - * @param {number} _message_id - 消息ID - * @returns {Promise} - */ - async RecallMessage (_contact, _message_id) { - throw new Error('Not implemented') - } - - /** - * 获取消息 - * @param {string?} _contact - ob11无需提供contact参数 - * @param {number} _message_id - 消息ID - * @returns {Promise} - 消息内容 - */ - async GetMessage (_contact, _message_id) { - throw new Error('Not implemented') - } - - /** - * 获取msg_id获取历史消息 - * @param {KarinContact} contact - 联系人信息 - * @param {string} start_message_id - 起始消息ID - * @param {number} count - 获取消息数量 - * @return {Promise>} - 消息内容 - * */ - async GetHistoryMessage (contact, start_message_id, count) { - throw new Error('Not implemented') - } - - /** - * todo - * 获取合并转发消息 - * @param {string} id - 合并转发 ID - * @returns {Promise} - 消息内容,使用消息的数组格式表示,数组中的消息段全部为 node 消息段 - */ - async get_forward_msg (id) { - throw new Error('Not implemented') - } - - /** - * 发送好友赞 - * @param {{ - * target_uid?: string, - * target_uin?: string, - * times: number - * }} options - * @param options.target_uid - 好友 QQ 号 任选其一 - * @param options.target_uin - 好友 QQ 号 任选其一 - * @param options.times - 赞的次数,默认为 10 - */ - async VoteUser (options) { - throw new Error('Not implemented') - } - - /** - * 群组踢人 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * reject_add_request?:boolean, - * kick_reason?:string - * }} options - * @param options.group_id - 群组ID - * @param options.target_uid - 被踢出目标的 uid 任选其一 - * @param options.target_uin - 被踢出目标的 uin 任选其一 - * @param options.reject_add_request - 是否拒绝再次申请,默认为false - * @param options.kick_reason - 踢出原因,可选 - * @returns {Promise} - 踢出操作的响应 - */ - async KickMember (options) { - throw new Error('Not implemented') - } - - /** - * 禁言用户 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * duration:number - * }} options - * @param options.group_id - 群组ID - * @param options.target_uid - 被禁言目标的uin 任选其一 - * @param options.target_uin - 被禁言目标的uid 任选其一 - * @param options.duration - 禁言时长(单位:秒) - * @returns {Promise} - 禁言操作的响应 - */ - async BanMember (options) { - throw new Error('Not implemented') - } - - /** - * 群组匿名用户禁言 - * @param {number} group_id - 群号 - * @param {object} anonymous - 要禁言的匿名用户对象(群消息上报的 anonymous 字段) - * @param {string} [anonymous_flag] - 要禁言的匿名用户的 flag(需从群消息上报的数据中获得) - * @param {number} [duration=1800] - 禁言时长,单位秒,无法取消匿名用户禁言 - */ - async set_group_anonymous_ban (group_id, anonymous, anonymous_flag, duration = 1800) { - throw new Error('Not implemented') - } - - /** - * 群组全员禁言 - * @param {number} group_id - 群号 - * @param {boolean} [enable=true] - 是否全员禁言 - */ - async SetGroupWholeBan (group_id, enable = true) { - throw new Error('Not implemented') - } - - /** - * 设置群管理员 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * is_admin:boolean - * }} options - 设置管理员选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 要设置为管理员的用户uid - * @param options.target_uin - 要设置为管理员的用户uin - * @param options.is_admin - 是否设置为管理员 - * @returns {Promise} - 设置群管理员操作的响应 - */ - async SetGroupAdmin (options) { - throw new Error('Not implemented') - } - - /** - * 群组匿名 - * @param {number} group_id - 群号 - * @param {boolean} [enable=true] - 是否允许匿名聊天 - */ - async set_group_anonymous (group_id, enable = true) { - throw new Error('Not implemented') - } - - /** - * 修改群名片 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * card:string - * }} options - * @param {number} options.group_id - 群组ID - * @param {string|number} options.target_uid - 目标用户的 uid 任选其一 - * @param {string|number} options.target_uin - 目标用户的 uin 任选其一 - * @param {string} options.card - 新的群名片 - * @returns {Promise} - 修改群名片操作的响应 - */ - async ModifyMemberCard (options) { - throw new Error('Not implemented') - } - - /** - * 设置群名 - * @param {number} group_id - 群号 - * @param {string} group_name - 新群名 - */ - async ModifyGroupName (group_id, group_name) { - throw new Error('Not implemented') - } - - /** - * 退出群组 - * @param {number} group_id - 群号 - * @param {boolean} [is_dismiss=false] - 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 - */ - async LeaveGroup (group_id, is_dismiss = false) { - throw new Error('Not implemented') - } - - /** - * 设置群专属头衔 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * unique_title:string - * }} options - 设置头衔选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 目标用户的uid - * @param options.target_uin - 目标用户的uin - * @param options.unique_title - 新的群头衔 - * @returns {Promise} - 设置群头衔操作的响应 - */ - async SetGroupUniqueTitle (options) { - throw new Error('Not implemented') - } - - /** - * 处理加好友请求 - * @param {string} flag - 加好友请求的 flag(需从上报的数据中获得) - * @param {boolean} [approve=true] - 是否同意请求 - * @param {string} [remark=''] - 添加后的好友备注(仅在同意时有效) - */ - async set_friend_add_request (flag, approve = true, remark = '') { - throw new Error('Not implemented') - } - - /** - * 处理加群请求/邀请 - * @param {string} flag - 加群请求的 flag(需从上报的数据中获得) - * @param {string} sub_event - add 或 invite,请求类型(需要和上报消息中的 sub_event 字段相符) - * @param {boolean} [approve=true] - 是否同意请求/邀请 - * @param {string} [reason=''] - 拒绝理由(仅在拒绝时有效) - */ - async set_group_add_request (flag, sub_event, approve = true, reason = '') { - throw new Error('Not implemented') - } - - /** - * 获取登录号信息 - * @returns {Promise<{account_uid:string, account_uin:string, account_name:number}>} - 登录号信息 - */ - async GetCurrentAccount () { - throw new Error('Not implemented') - } - - /** - * 获取陌生人信息 不支持批量获取 - * @param {Object} [options] - 陌生人信息选项 - * @param {Array} [options.target_uids] - 目标用户的 uid 数组 可选 - * @param {Array} [options.target_uins] - 目标用户的 uin 数组 可选 - * @param {boolean} [options.no_cache=false] - 是否不使用缓存 - * @returns {Promise} - 获取到的陌生人信息 - */ - async GetStrangerProfileCard (options) { - throw new Error('Not implemented') - } - - /** - * 获取好友列表 - * @returns {Promise>} - 好友列表 - */ - async GetFriendList () { - throw new Error('Not implemented') - } - - /** - * 获取群信息 - * @param {number} group_id - 群号 - * @param {boolean} [no_cache=false] - 是否不使用缓存 - * @returns {Promise} - 群信息 - */ - async GetGroupInfo (group_id, no_cache = false) { - throw new Error('Not implemented') - } - - /** - * 获取群列表 - * @returns {Promise>} - 群列表 - */ - async GetGroupList () { - throw new Error('Not implemented') - } - - /** - * 获取群成员信息 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * refresh?:boolean - * }} options - 获取成员信息选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 目标用户的uid - * @param options.target_uin - 目标用户的uin - * @param options.refresh - 是否刷新缓存,默认为 false - * @returns {Promise} - 获取群成员信息操作的响应 - */ - async GetGroupMemberInfo (options) { - throw new Error('Not implemented') - } - - /** - * 获取群成员列表 - * @param {Object} options - 获取成员列表选项 - * @param {number} options.group_id - 群组ID - * @param {boolean} [options.refresh] - 是否刷新缓存 - * @returns {Promise} - 获取群成员列表操作的响应 - */ - async GetGroupMemberList (options) { - throw new Error('Not implemented') - } - - /** - * 获取群荣誉信息 - * @param {Object} options - 获取群荣誉信息选项 - * @param {number} options.group_id - 群号 - * @param {boolean} [options.refresh] - 是否刷新缓存 - * @returns {Promise} - 获取群荣誉信息操作的响应 - */ - async get_group_honor_info (options) { - throw new Error('Not implemented') - } - - /** - * 对消息进行表情回应 - * @param {KarinContact} Contact - 联系人信息 - * @param {string} message_id - 消息ID - * @param {string} face_id - 表情ID - */ - async ReactMessageWithEmojiRequest (Contact, message_id, face_id, is_set = true) { - throw new Error('Not implemented') - } - - /** - * 上传群文件 - * @param {string} group_id - 群号 - * @param {string} file - 本地文件绝对路径 - * @param {string} name - 文件名称 必须提供 - * @param {string} [folder] - 父目录ID 不提供则上传到根目录 - */ - async UploadGroupFile (group_id, file, name) { - throw new Error('Not implemented') - } - - /** - * 上传私聊文件 - * @param {string} user_id - 用户ID - * @param {string} file - 本地文件绝对路径 - * @param {string} name - 文件名称 必须提供 - */ - async UploadPrivateFile (user_id, file, name) { - throw new Error('Not implemented') - } - - /** - * 获取 Cookies - * @param {string} domain - 需要获取 cookies 的域名 - * @returns {Promise<{string}>} - Cookies - */ - async get_cookies (domain) { - throw new Error('Not implemented') - } - - /** - * 获取 CSRF Token - * @returns {Promise} - CSRF Token - */ - async get_csrf_token () { - throw new Error('Not implemented') - } - - /** - * 获取 QQ 相关接口凭证 - * @param {string} domain - 需要获取 cookies 的域名 - * @returns {Promise<{ - * cookies: string, - * cstf_token: number - * }>} - QQ 相关接口凭证 - */ - async get_credentials (domain) { - throw new Error('Not implemented') - } - - /** - * 获取语音 - * @param {string} file - 收到的语音文件名 - * @param {string} out_format - 要转换到的格式 - * @returns {Promise<{ - * file: string, - * out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' - * }>} - 转换后的语音文件路径 - */ - async get_record (file, out_format) { - throw new Error('Not implemented') - } - - /** - * 获取图片 - * @param {string} file - 收到的图片文件名 - * @returns {Promise<{ - * size: number, - * filename: string, - * url: string - * }>} - 下载后的图片文件路径 - */ - async get_image (file) { - throw new Error('Not implemented') - } - - /** - * 检查是否可以发送图片 - * @returns {Promise} - 是否可以发送图片 - */ - async can_send_image () { - throw new Error('Not implemented') - } - - /** - * 检查是否可以发送语音 - * @returns {Promise} - 是否可以发送语音 - */ - async can_send_record () { - throw new Error('Not implemented') - } - - /** - * 获取运行状态 - * @returns {Promise} - 运行状态 - */ - async get_status () { - throw new Error('Not implemented') - } - - /** - * 获取版本信息 - * @returns {Promise} - 版本信息 - */ - async GetVersion () { - throw new Error('Not implemented') - } - - /** - * 发送合并转发消息 - * @param {KarinContact} contact - * @param {Array} elements - * @return {Promise<{message_id}>} - */ - async sendForwardMessage (contact, elements) { - let message_id = await this.UploadForwardMessage(contact, elements) - if (this.version.name === 'Lagrange.OneBot') { - message_id = await this.SendMessage(contact, [segment.forward(message_id)]) - } - return { message_id } - } -} diff --git a/lib/adapter/kritor/api.js b/lib/adapter/kritor/api.js deleted file mode 100644 index 70aab3b..0000000 --- a/lib/adapter/kritor/api.js +++ /dev/null @@ -1,853 +0,0 @@ -import { kritor } from 'kritor-proto' -import { KarinAdapter } from '../adapter.js' - -/** - * @extends KarinAdapter - */ -export default class Kritor extends KarinAdapter { - constructor (grpc, account, logger, common, config, listener) { - super() - /** - * @type {import('@grpc/grpc-js').ServerReadableStream} - */ - this.grpc = grpc - /** 自增 */ - this.seq = 0 - const { uid, uin } = account - this.account.uid = uid - this.account.uin = uin - - /** 适配器信息 */ - this.adapter.id = 'QQ' - this.adapter.name = 'Kritor' - this.adapter.type = 'grpc' - this.adapter.sub_type = 'server' - - /** 监听响应事件 */ - grpc.on('data', data => listener.emit(data.seq, data)) - /** 监听关闭事件 */ - grpc.on('end', () => this.logger('warn', '连接已断开')) - this.logger = logger - this.common = common - this.config = config - this.listener = listener - this.#init() - } - - async #init () { - const { account_name: name } = await this.GetCurrentAccount() - this.account.name = name - const { app_name, version } = await this.GetVersion() - this.version.name = app_name - this.version.app_name = app_name - this.version.version = version - } - - // ============辅助============= - - /** - * 编码 - * @param {string} service - 服务名 - * @param {string} cmd - 命令名 - * @param {string} type - 类型 - * @param {object} buf - 数据 - * @returns {Uint8Array} - */ - encode (service, cmd, type, buf) { - buf = kritor[type][`${cmd}Request`].encode(buf).finish() - cmd = `${service}.${cmd}` - return { cmd, buf } - } - - /** 解码 */ - decode (cmd, type, buf) { - return kritor[type][`${cmd}Response`].decode(buf) - } - - /** - * todo 补充类型 - * @param elements - * @return {*[]} - */ - elements (elements) { - const _elements = [] - const ElementType = kritor.common.Element.ElementType - for (const i of elements) { - switch (i.type) { - case 'text': { - const { TEXT: type } = ElementType - const { text } = i - _elements.push(new kritor.common.TextElement({ type, text: { text } })) - break - } - case 'image': { - const { IMAGE: type } = ElementType - const { file } = i - _elements.push(new kritor.common.ImageElement({ type, image: { file } })) - break - } - case 'at': { - const { AT: type } = ElementType - const { uid, uin = '' } = i - _elements.push(new kritor.common.AtElement({ type, at: { uid, uin } })) - break - } - case 'face': { - const { FACE: type } = ElementType - const { id, is_big = false } = i - _elements.push(new kritor.common.FaceElement({ type, face: { id, is_big } })) - break - } - case 'reply': { - const { REPLY: type } = ElementType - const { id } = i - _elements.push(new kritor.common.ReplyElement({ type, reply: { id } })) - break - } - case 'voice': { - const { VOICE: type } = ElementType - const { file } = i - _elements.push(new kritor.common.VoiceElement({ type, voice: { file } })) - break - } - } - } - - return _elements - } - - // ================消息事件api============ - - /** - * 发送消息 - * @param {KarinContact} contact - * @param {Array} elements 消息元素 - * @param {number} retry_count 重试次数 - * @returns {Promise<{message_id:string}>} - */ - async SendMessage (contact, elements, retry_count = 1) { - const service = 'MessageService' - const cmd = 'SendMessage' - const type = 'message' - elements = this.elements(elements) - const buf = { contact, elements, retry_count } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 撤回消息 */ - async RecallMessage (contact, message_id) { - const service = 'MessageService' - const cmd = 'RecallMessage' - const type = 'message' - const buf = { contact, message_id } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 设置消息评论表情 */ - async ReactMessageWithEmoji (contact, message_id, face_id, is_set) { - const service = 'MessageService' - const cmd = 'ReactMessageWithEmoji' - const type = 'message' - const buf = { contact, message_id, face_id, is_set } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 获取msg_id获取消息 */ - async GetMessage (contact, message_id) { - const service = 'MessageService' - const cmd = 'GetMessage' - const type = 'message' - const buf = { contact, message_id } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 通过seq获取消息 */ - async GetMessageBySeq (contact, message_seq) { - const service = 'MessageService' - const cmd = 'GetMessageBySeq' - const type = 'message' - const buf = { contact, message_seq } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 获取msg_id获取历史消息 */ - async GetHistoryMessage (contact, start_message_id, count) { - const service = 'MessageService' - const cmd = 'GetHistoryMessage' - const type = 'message' - const buf = { contact, start_message_id, count } - const res = await this.Serialization(service, cmd, type, buf) - return res.messages - } - - /** 通过seq获取历史消息 */ - async GetHistoryMessageBySeq (contact, start_message_seq, count) { - const service = 'MessageService' - const cmd = 'GetHistoryMessageBySeq' - const type = 'message' - const buf = { contact, start_message_seq, count } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 清空本地聊天记录 */ - async SetMessageReaded (contact) { - const service = 'MessageService' - const cmd = 'SetMessageReaded' - const type = 'message' - const buf = { contact } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 上传合并转发消息 */ - async UploadForwardMessage (contact, messages, retry_count = 1) { - const service = 'MessageService' - const cmd = 'UploadForwardMessage' - const type = 'message' - messages = this.elements(messages) - const buf = { contact, messages, retry_count } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 下载合并转发消息 */ - async DownloadForwardMessage (res_id) { - const service = 'MessageService' - const cmd = 'DownloadForwardMessage' - const type = 'message' - const buf = { res_id } - const res = await this.Serialization(service, cmd, type, buf) - res.messages = this.elements(res.messages) - return res - } - - /** 获取精华消息 */ - async GetEssenceMessageList (group_id, page, page_size) { - const service = 'MessageService' - const cmd = 'GetEssenceMessageList' - const type = 'message' - const buf = { group_id, page, page_size } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 设置精华消息 */ - async SetEssenceMessage (group_id, message_id) { - const service = 'MessageService' - const cmd = 'SetEssenceMessage' - const type = 'message' - const buf = { group_id, message_id } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** 删除精华消息 */ - async DeleteEssenceMessage (group_id, message_id) { - const service = 'MessageService' - const cmd = 'DeleteEssenceMessage' - const type = 'message' - const buf = { group_id, message_id } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - // ========================其他api=============== - - logger (level, ...args) { - this.logger.bot(level, this.account.uin, ...args) - } - - /** - * 获取用户头像 - * @param {string} uid - 用户id,默认为发送者uid - * @param {number} [size] - 头像大小,默认`0` - * @returns {string} - 头像的url地址 - */ - getAvatarUrl (uid, size = 0) { - return `https://q1.qlogo.cn/g?b=qq&s=${size}&nk=` + uid - } - - /** - * 获取群头像 - * @param {string} group_id - 群号 - * @param {number} [size] - 头像大小,默认`0` - * @param {number} [history] - 历史头像记录,默认`0`,若要获取历史群头像则填写1,2,3... - * @returns {string} - 群头像的url地址 - */ - getGroupAvatar (group_id, size = 0, history = 0) { - return `https://p.qlogo.cn/gh/${group_id}/${group_id}${history ? '_' + history : ''}/` + size - } - - // =============核心api=============== - - /** - * 获取Kritor版本 - * @returns {Promise} - */ - async GetVersion () { - const service = 'CoreService' - const cmd = 'GetVersion' - const type = 'core' - const buf = {} - /** @type {GetVersionResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 让Kritor下载文件到Kritor本地 - * @param {string} file - url或者base64 - * @param {'url'|'base64'} file_type - 文件类型 - * @param {string} [root_path] - 下载文件的根目录 需要保证Kritor有该目录访问权限 可选 - * @param {string} [file_name] - 保存的文件名称 默认为文件MD5 可选 - * @param {Number} [thread_cnt] - 下载文件的线程数 默认为3 可选 - * @param {string} [headers] - 下载文件的请求头 可选 - * @returns {} - 下载成功的结果 - */ - async DownloadFile (file, file_type, root_path, file_name, thread_cnt = 3, headers) { - const service = 'CoreService' - const cmd = 'DownloadFile' - const type = 'core' - const buf = { [file_type]: file, root_path, file_name, thread_cnt, headers } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 获取当前账户的信息 - * @returns {Promise<{account_uid:string, account_uin:string, account_name:number}>} - */ - async GetCurrentAccount () { - const service = 'CoreService' - const cmd = 'GetCurrentAccount' - const type = 'core' - const buf = {} - const res = await this.Serialization(service, cmd, type, buf) - /** 统一使用str */ - res.account_uin = String(res.account_uin) - return res - } - - /** 切换账户 */ - async SwitchAccount () { - this.logger('warn', '暂未实现...') - } - - // =============好友服务=========== - /** - * 获取好友列表 - * @param {boolean} [refresh] - 是否刷新好友列表 可选 - * @returns {Promise}} - 获取到的好友列表信息 - */ - async GetFriendList (refresh) { - const service = 'FriendService' - const cmd = 'GetFriendList' - const type = 'friend' - const buf = { refresh } - /** @type {GetFriendListResponse} */ - const res = await this.Serialization(service, cmd, type, buf) - return res.friends_info - } - - /** - * 获取好友名片信息 - * @param {Object} [options] - 名片信息选项 - * @param {Array} [options.target_uids] - 目标用户的 uid 数组 可选 - * @param {Array} [options.target_uins] - 目标用户的 uin 数组 可选 - * @returns {Promise<{friend_cards:Array}>} - 获取到的好友名片信息 - */ - async GetFriendProfileCard (options) { - const service = 'FriendService' - const cmd = 'GetFriendProfileCard' - const type = 'friend' - const buf = options - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 获取陌生人信息 - * @param {Object} [options] - 陌生人信息选项 - * @param {Array} [options.target_uids] - 目标用户的 uid 数组 可选 - * @param {Array} [options.target_uins] - 目标用户的 uin 数组 可选 - * @returns {Promise>} - 获取到的陌生人信息 - */ - async GetStrangerProfileCard (options) { - const service = 'FriendService' - const cmd = 'GetStrangerProfileCard' - const type = 'friend' - const buf = options - /** @type {GetStrangerProfileCardResponse} */ - const res = await this.Serialization(service, cmd, type, buf) - return res.strangers_profile_card - } - - /** - * 设置自己的名片 - * @param {Object} [options] - 名片信息对象 - * @param {string} [options.nick_name] - 昵称 可选 - * @param {string} [options.company] - 公司 可选 - * @param {string} [options.email] - 邮箱 可选 - * @param {string} [options.college] - 学校 可选 - * @param {string} [options.personal_note] - 个人备注 可选 - * @param {number} [options.birthday] - 生日 可选 - * @param {number} [options.age] - 年龄 可选 - * @returns {Promise} - 设置名片的响应 - */ - async SetProfileCard (options) { - const service = 'FriendService' - const cmd = 'SetProfileCard' - const type = 'friend' - const buf = options || {} - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 判断是否是黑名单用户 - * @param {string} [target_uid] - 目标用户的 uid 任选其一提供 - * @param {string} [target_uin] - 目标用户的 uin 任选其一提供 - * @returns {Promise<{is_black_list_user:boolean}>} - 是否是黑名单用户的结果 - */ - async IsBlackListUser (target_uid, target_uin) { - const service = 'FriendService' - const cmd = 'IsBlackListUser' - const type = 'friend' - const buf = { target_uid, target_uin } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 点赞好友 - * @param {{ - * target_uid?:string, - * target_uin?:string, - * vote_count:number - * }} options - uid 或者 uin 任选其一提供 - * @param options.target_uid - 目标用户的 uid - * @param options.target_uin - 目标用户的 uin - * @param options.vote_count - 点赞数 - * @returns {Promise} - 点赞的响应 - */ - async VoteUser (options) { - const service = 'FriendService' - const cmd = 'VoteUser' - const type = 'friend' - const buf = options - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 根据 uin 获取 uid - * @param {Array} [target_uins] - 目标用户的 uin 数组 - * @returns {Promise<{uid_map:Map}>} - 获取到的 uid 映射表 - */ - async GetUidByUin (target_uins) { - const service = 'FriendService' - const cmd = 'GetUidByUin' - const type = 'friend' - const buf = { target_uins } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 根据 uid 获取 uin - * @param {Array} [target_uids] - 目标用户的 uid 数组 - * @returns {Promise<{uin_map:Map}>} - 获取到的 uin 映射表 - */ - async GetUinByUid (target_uids) { - const service = 'FriendService' - const cmd = 'GetUinByUid' - const type = 'friend' - const buf = { target_uids } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - // ============群组服务============= - /** - * 禁言用户 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * duration:number - * }} options - * @param options.group_id - 群组ID - * @param options.target_uid - 被禁言目标的uin 任选其一 - * @param options.target_uin - 被禁言目标的uid 任选其一 - * @param options.duration - 禁言时长(单位:秒) - * @returns {Promise} - 禁言操作的响应 - */ - async BanMember (options) { - const service = 'GroupService' - const cmd = 'BanMember' - const type = 'group' - const buf = options - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 戳一戳用户头像 - * @param {Object} options - 戳一戳选项 - * @param {string} options.group_id - 群组ID - * @param {string} [options.target_uid] - 被戳一戳目标的 uid 任选其一 - * @param {string} [options.target_uin] - 被戳一戳目标的 uin 任选其一 - * @returns {Promise} - 戳一戳操作的响应 - */ - async PokeMember (options) { - const service = 'GroupService' - const cmd = 'PokeMember' - const type = 'group' - const buf = options - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 群组踢人 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * reject_add_request?:boolean, - * kick_reason?:string - * }} options - * @param options.group_id - 群组ID - * @param options.target_uid - 被踢出目标的 uid 任选其一 - * @param options.target_uin - 被踢出目标的 uin 任选其一 - * @param options.reject_add_request - 是否拒绝再次申请,默认为false - * @param options.kick_reason - 踢出原因,可选 - * @returns {Promise} - 踢出操作的响应 - */ - async KickMember (options) { - const service = 'GroupService' - const cmd = 'KickMember' - const type = 'group' - const buf = options - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 退出群组 - * @param {number} group_id - 群组ID - * @returns {Promise} - 退出群组操作的响应 - */ - async LeaveGroup (group_id) { - const service = 'GroupService' - const cmd = 'LeaveGroup' - const type = 'group' - const buf = { group_id } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 修改群名片 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * card:string - * }} options - * @param {number} options.group_id - 群组ID - * @param {string|number} options.target_uid - 目标用户的 uid 任选其一 - * @param {string|number} options.target_uin - 目标用户的 uin 任选其一 - * @param {string} options.card - 新的群名片 - * @returns {Promise} - 修改群名片操作的响应 - */ - async ModifyMemberCard (options) { - const service = 'GroupService' - const cmd = 'ModifyMemberCard' - const type = 'group' - const buf = options - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 修改群名称 - * @param {number} group_id - 群组ID - * @param {string} group_name - 新的群名称 - * @returns {Promise} - 修改群名称操作的响应 - */ - async ModifyGroupName (group_id, group_name) { - const service = 'GroupService' - const cmd = 'ModifyGroupName' - const type = 'group' - const buf = { group_id, group_name } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 修改群备注 - * @param {number} group_id - 群组ID - * @param {string} remark - 新的群备注 - * @returns {Promise} - 修改群备注操作的响应 - */ - async ModifyGroupRemark (group_id, remark) { - const service = 'GroupService' - const cmd = 'ModifyGroupRemark' - const type = 'group' - const buf = { group_id, remark } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 设置群管理员 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * is_admin:boolean - * }} options - 设置管理员选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 要设置为管理员的用户uid - * @param options.target_uin - 要设置为管理员的用户uin - * @param options.is_admin - 是否设置为管理员 - * @returns {Promise} - 设置群管理员操作的响应 - */ - async SetGroupAdmin (options) { - const service = 'GroupService' - const cmd = 'SetGroupAdmin' - const type = 'group' - const buf = options - /** @type {SetGroupAdminResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 设置群头衔 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * unique_title:string - * }} options - 设置头衔选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 目标用户的uid - * @param options.target_uin - 目标用户的uin - * @param options.unique_title - 新的群头衔 - * @returns {Promise} - 设置群头衔操作的响应 - */ - async SetGroupUniqueTitle (options) { - const service = 'GroupService' - const cmd = 'SetGroupUniqueTitle' - const type = 'group' - const buf = options - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 设置全员禁言 - * @param {number} group_id - 群组ID - * @param {boolean} is_ban - 是否全员禁言 - * @returns {Promise} - 设置全员禁言操作的响应 - */ - async SetGroupWholeBan (group_id, is_ban) { - const service = 'GroupService' - const cmd = 'SetGroupWholeBan' - const type = 'group' - const buf = { group_id, is_ban } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 获取群信息 - * @param {number} group_id - 群组ID - * @param {boolean?} no_cache - * @returns {Promise} - 获取群信息操作的响应 - */ - async GetGroupInfo (group_id, no_cache = false) { - const service = 'GroupService' - const cmd = 'GetGroupInfo' - const type = 'group' - const buf = { group_id } - /** @type {GetGroupInfoResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res.group_info - } - - /** - * 获取群列表 - * @param {boolean} [refresh] - 是否刷新缓存 - * @returns {Promise} - 获取群列表操作的响应 - */ - async GetGroupList (refresh) { - const service = 'GroupService' - const cmd = 'GetGroupList' - const type = 'group' - const buf = { refresh } - /** @type {GetGroupListResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res.groups_info - } - - /** - * 获取群成员信息 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * refresh?:boolean - * }} options - 获取成员信息选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 目标用户的uid - * @param options.target_uin - 目标用户的uin - * @param options.refresh - 是否刷新缓存,默认为 false - * @returns {Promise} - 获取群成员信息操作的响应 - */ - async GetGroupMemberInfo (options) { - const service = 'GroupService' - const cmd = 'GetGroupMemberInfo' - const type = 'group' - const buf = options - /** @type {GetGroupMemberInfoResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res.group_member_info - } - - /** - * 获取群成员列表 - * @param {Object} options - 获取成员列表选项 - * @param {number} options.group_id - 群组ID - * @param {boolean} [options.refresh] - 是否刷新缓存 - * @returns {Promise} - 获取群成员列表操作的响应 - */ - async GetGroupMemberList (options) { - const service = 'GroupService' - const cmd = 'GetGroupMemberList' - const type = 'group' - const buf = { - group_id: options.group_id, - refresh: options.refresh || false, - } - /** @type {GetGroupMemberListResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res.group_members_info - } - - /** - * 获取禁言用户列表 - * @param {number} group_id - 群组ID - * @returns {Promise} - 获取禁言用户列表操作的响应 - */ - async GetProhibitedUserList (group_id) { - const service = 'GroupService' - const cmd = 'GetProhibitedUserList' - const type = 'group' - const buf = { group_id } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 获取艾特全体成员剩余次数 - * @param {number} group_id - 群组ID - * @returns {Promise} - 获取艾特全体成员剩余次数操作的响应 - */ - async GetRemainCountAtAll (group_id) { - const service = 'GroupService' - const cmd = 'GetRemainCountAtAll' - const type = 'group' - const buf = { group_id } - const res = await this.Serialization(service, cmd, type, buf) - return res - } - - /** - * 获取未加入群组信息 - * @param {number} group_id - 群号 - * @returns {Promise} - 获取未加入群组信息操作的响应 - */ - async GetNotJoinedGroupInfo (group_id) { - const service = 'GroupService' - const cmd = 'GetNotJoinedGroupInfo' - const type = 'group' - const buf = { group_id } - /** @type {GetNotJoinedGroupInfoResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res.group_info - } - - /** - * 获取群荣誉信息 - * @param {Object} options - 获取群荣誉信息选项 - * @param {number} options.group_id - 群号 - * @param {boolean} [options.refresh] - 是否刷新缓存 - * @returns {Promise} - 获取群荣誉信息操作的响应 - */ - async get_group_honor_info (options) { - const service = 'GroupService' - const cmd = 'GetGroupHonor' - const type = 'group' - const buf = { - group_id: options.group_id, - refresh: options.refresh || false, - } - /** @type {GetGroupHonorResponse} **/ - const res = await this.Serialization(service, cmd, type, buf) - return res.group_honors_info - } - - /** - * 序列化请求后发送请求,并反序列化响应 - * @param {string} service - 服务名 - * @param {string} cmd - 命令名 - * @param {string} type - 类型 - * @param {object} buf - 请求参数 - * @returns {Promise} - */ - async Serialization (service, cmd, type, buf) { - let data = this.encode(service, cmd, type, buf) - data = await this.SendApi(data.cmd, data.buf) - const res = this.decode(cmd, type, data.buf) - return res - } - - /** - * 发送Api请求 - * @param {string} cmd - 命令 - * @param {protobuf} buf - 请求参数 - * @param {number} time - 请求超时时间 默认10s - * @returns {Promise} - */ - SendApi (cmd, buf, time) { - if (!time) time = this.config.timeout('grpc') - const seq = this.seq - /** 立刻自增防止并发导致重复的seq... */ - this.seq++ - const params = { cmd, seq, buf } - this.grpc.write(params) - return new Promise((resolve, reject) => { - const listener = (data) => { - data.code === 'SUCCESS' ? resolve(data) : resolve(data) - this.removeListener(seq, listener) - } - - listener.once(seq, listener) - - /** 超时 */ - setTimeout(() => { - this.removeListener(seq, listener) - reject(new Error('超时未响应...')) - }, time * 1000) - }) - } -} diff --git a/lib/adapter/kritor/converter.js b/lib/adapter/kritor/converter.js deleted file mode 100644 index a48fdfa..0000000 --- a/lib/adapter/kritor/converter.js +++ /dev/null @@ -1,423 +0,0 @@ -import { - KarinFileElement, - KarinAtElement, - KarinFaceElement, - KarinImageElement, - KarinTextElement, - KarinReplyElement, - KarinBubbleFaceElement, - KarinRecordElement, - KarinBasketballElement, - KarinvideoElement, - KarinDiceElement, - KarinRpsElement, - KarinPokeElement, - KarinMusicElement, - KarinWeatherElement, - KarinLocationElement, - KarinShareElement, - KarinGiftElement, - KarinForwardElement, - KarinXmlElement, - KarinJsonElement, -} from '../../bot/KarinElement.js' - -import { - KarinFriendPokeNotice, - KarinFriendRecallNotice, - KarinFriendFileUploadedNotice, - KarinGroupPokeNotice, - KarinGoupCardChangedNotice, - KarinGroupUniqueTitleChangedNotice, - KarinGroupEssenceMessageNotice, - KarinGroupRecallNotice, - KarinGroupMemberIncreasedNotice, - KarinGroupMemberDecreasedNotice, - KarinGroupAdminChangedNotice, - KarinGroupMemberBanNotice, - KarinGroupSignInNotice, - KarinGroupWholeBanNotice, - KarinGroupFileUploadedNotice, -} from '../../bot/KarinNotice.js' - -import { - KarinFriendApplyRequest, - KarinGroupApplyRequest, - KarinInvitedJoinGroupRequest, -} from '../../bot/KarinRequest.js' - -import { - KarinMessage, -} from '../../bot/KarinMessage.js' - -import { logger, Bot, kritor } from '#Karin' - -// import { raw } from 'express' - -/** - * 抽象转换器 - */ -export class Converter { - convert (raw) { - } -} - -/** - * Kritor 消息转换器 - */ -export class MessageConverter extends Converter { - /** - * - * @param {kritor.common.PushMessageBody} data - * @param {string} self_id 机器人自身的id - * @return {KarinMessage} - */ - convert (data, self_id) { - const time = data.time - const message_id = data.message_id - const message_seq = data.message_seq - const contact = data.contact - const sender = data.sender - /** scene映射表 */ - const sceneMap = { - [kritor.common.Scene.GROUP]: 'group', - [kritor.common.Scene.FRIEND]: 'friend', - [kritor.common.Scene.GUILD]: 'guild', - [kritor.common.Scene.NEARBY]: 'nearby', - [kritor.common.Scene.STRANGER]: 'stranger', - [kritor.common.Scene.STRANGER_FROM_GROUP]: 'stranger_from_group', - } - /* - 0=群聊 1=私聊 2=频道 5=附近的人 6=陌生人 10=群临时会话 - 0=group 1=friend 2=guild 5=nearby 6=stranger 10=stranger_from_group - */ - const scene = sceneMap[contact.scene] - - /** - * - * @type {Array} - */ - const elements = [] - - for (const i of data.elements) { - switch (i.type) { - /** 文本消息 */ - case kritor.common.Element.ElementType.TEXT: { - elements.push(new KarinTextElement(i.text.text)) - break - } - /** 艾特消息 */ - case kritor.common.Element.ElementType.AT: { - elements.push(new KarinAtElement(i.at.uid, i.at.uin)) - break - } - /** 表情消息 */ - case kritor.common.Element.ElementType.FACE: { - elements.push(new KarinFaceElement(i.face.id, i.face.is_big)) - break - } - /** 图片消息 */ - case kritor.common.Element.ElementType.IMAGE: { - const { file_url, ...args } = i.image - elements.push(new KarinImageElement(file_url, ...args)) - break - } - /** 弹射表情 */ - case kritor.common.Element.ElementType.BUBBLE_FACE: { - elements.push(new KarinBubbleFaceElement(i.bubble_face.id, i.bubble_face.count)) - break - } - /** 回复消息 */ - case kritor.common.Element.ElementType.REPLY: { - elements.push(new KarinReplyElement(i.reply.message_id)) - break - } - /** 语音消息 */ - case kritor.common.Element.ElementType.VOICE: { - const { file_url, ...args } = i.voice - elements.push(new KarinRecordElement(file_url, ...args)) - break - } - /** 视频消息 */ - case kritor.common.Element.ElementType.VIDEO: { - const { file_url, ...args } = i.video - elements.push(new KarinvideoElement(file_url, ...args)) - break - } - /** 篮球消息 */ - case kritor.common.Element.ElementType.BASKETBALL: { - elements.push(new KarinBasketballElement(i.basketball.id)) - break - } - /** 骰子消息 */ - case kritor.common.Element.ElementType.DICE: { - elements.push(new KarinDiceElement(i.dice.id)) - break - } - case kritor.common.Element.ElementType.RPS: { - elements.push(new KarinRpsElement(i.rps.id)) - break - } - /** 戳一戳消息 */ - case kritor.common.Element.ElementType.POKE: { - const { id = 1, type = 1, strength = 1 } = i.poke - elements.push(new KarinPokeElement(type, id, strength)) - break - } - /** 音乐消息 */ - case kritor.common.Element.ElementType.MUSIC: { - const { platform, id = '', custom = '' } = i.music - elements.push(new KarinMusicElement(platform, id || custom)) - break - } - /** 天气消息 */ - case kritor.common.Element.ElementType.WEATHER: { - const { city, code } = i.weather - elements.push(new KarinWeatherElement(city, code)) - break - } - /** 位置消息 */ - case kritor.common.Element.ElementType.LOCATION: { - const { title = '', address = '', lat, lon } = i.location - elements.push(new KarinLocationElement(lat, lon, title, address)) - break - } - /** 链接分享 */ - case kritor.common.Element.ElementType.SHARE: { - const { url, title, content, image } = i.share - elements.push(new KarinShareElement(url, title, content, image)) - break - } - /** 礼物消息 只收不发 */ - case kritor.common.Element.ElementType.GIFT: { - const { qq, id } = i.gift - elements.push(new KarinGiftElement(qq, id)) - break - } - /** 转发消息 */ - case kritor.common.Element.ElementType.FORWARD: { - const { res_id, uniseq = '', summary = '', description = '' } = i.forward - elements.push(new KarinForwardElement(res_id, uniseq, summary, description)) - break - } - /** 文件消息 只收不发 */ - case kritor.common.Element.ElementType.FILE: { - elements.push(new KarinFileElement(i.file.file_url)) - break - } - /** JSON消息 */ - case kritor.common.Element.ElementType.JSON: { - elements.push(new KarinJsonElement(i.json.json)) - break - } - /** XML消息 */ - case kritor.common.Element.ElementType.XML: { - elements.push(new KarinXmlElement(i.xml.xml)) - break - } - // 这都啥玩意啊... - case kritor.common.Element.ElementType.MARKET_FACE: - case kritor.common.Element.ElementType.CONTACT: - case kritor.common.Element.ElementType.MARKDOWN: - case kritor.common.Element.ElementType.KEYBOARD: - default: { - let { ...args } = i - args = JSON.stringify(args) - logger.warn(`未知消息类型 ${i.type} ${args}`) - elements.push(new KarinTextElement(args)) - } - } - } - - // user_id与peer统一使用uid - const e = new KarinMessage({ - self_id, - user_id: sender.uid, - group_id: contact.peer || '', - time, - message_id, - message_seq, - sender, - elements, - contact: { - scene, - peer: contact.peer, - sub_peer: contact.sub_peer, - }, - }) - - /** - * 快速回复 开发者不应该使用这个方法,应该使用由karin封装过后的reply方法 - * @param {Array} elements - * @param {number} retry_count 重试次数 - * @returns {Promise} - */ - e.replyCallback = (elements, retry_count) => Bot.adapter[self_id].SendMessage(e.contact, elements, retry_count) - return e - } -} - -/** - * Kritor 通知事件转换器 - */ -export class NoticeConverter extends Converter { - /** - * - * @param {kritor.event.NoticeEvent} data - * @param {string} self_id 机器人自身的id - * @return {KarinNotice} - */ - convert (data, self_id) { - super.convert(data, self_id) - let e = {} - switch (data.type) { - /** 好友头像戳一戳 */ - case kritor.event.NoticeEvent.NoticeType.PRIVATE_POKE: { - e = new KarinFriendPokeNotice({ self_id, time: data.time, content: data.private_poke }) - break - } - /** 好友消息撤回 */ - case kritor.event.NoticeEvent.NoticeType.PRIVATE_RECALL: { - e = new KarinFriendRecallNotice({ self_id, time: data.time, content: data.private_recall }) - break - } - /** 私聊文件上传 */ - case kritor.event.NoticeEvent.NoticeType.PRIVATE_FILE_UPLOADED: { - e = new KarinFriendFileUploadedNotice({ self_id, time: data.time, content: data.private_file_uploaded }) - break - } - /** 群头像戳一戳 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_POKE: { - e = new KarinGroupPokeNotice({ self_id, time: data.time, content: data.group_poke }) - break - } - /** 群名片改变 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_CARD_CHANGED: { - e = new KarinGoupCardChangedNotice({ self_id, time: data.time, content: data.group_card_changed }) - break - } - - /** 群成员专属头衔改变 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED: { - e = new KarinGroupUniqueTitleChangedNotice({ self_id, time: data.time, content: data.group_member_unique_title_changed }) - break - } - /** 群精华消息改变 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_ESSENCE_CHANGED: { - e = new KarinGroupEssenceMessageNotice({ self_id, time: data.time, content: data.group_essence_changed }) - break - } - /** 群消息撤回 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_RECALL: { - e = new KarinGroupRecallNotice({ self_id, time: data.time, content: data.group_recall }) - break - } - /** 群成员增加 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_MEMBER_INCREASE: { - e = new KarinGroupMemberIncreasedNotice({ self_id, time: data.time, content: data.group_member_increase }) - break - } - /** 群成员减少 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_MEMBER_DECREASE: { - e = new KarinGroupMemberDecreasedNotice({ self_id, time: data.time, content: data.group_member_decrease }) - break - } - /** 群管理员变动 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_ADMIN_CHANGED: { - e = new KarinGroupAdminChangedNotice({ self_id, time: data.time, content: data.group_admin_changed }) - break - } - /** 群成员被禁言 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_MEMBER_BAN: { - e = new KarinGroupMemberBanNotice({ self_id, time: data.time, content: data.group_member_ban }) - break - } - /** 群签到 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_SIGN_IN: { - e = new KarinGroupSignInNotice({ self_id, time: data.time, content: data.group_sign_in }) - break - } - /** 群全员禁言 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_WHOLE_BAN: { - e = new KarinGroupWholeBanNotice({ self_id, time: data.time, content: data.group_whole_ban }) - break - } - /** 群文件上传 */ - case kritor.event.NoticeEvent.NoticeType.GROUP_FILE_UPLOADED: { - e = new KarinGroupFileUploadedNotice({ self_id, time: data.time, content: data.group_file_uploaded }) - break - } - } - /** - * 快速回复 开发者不应该使用这个方法,应该使用由karin封装过后的reply方法 - * @param {Array} elements - * @param {number} retry_count 重试次数 - * @returns {Promise} - */ - e.replyCallback = (elements, retry_count) => Bot.adapter[self_id].SendMessage(e.contact, elements, retry_count) - return e - } -} - -/** - * Kritor 请求事件转换器 - */ -export class RequestConverter extends Converter { - /** - * - * @param {kritor.event.RequestEvent} raw - * @return {KarinRequest|null} - */ - convert (data, self_id) { - super.convert(data, self_id) - let e = {} - switch (data.type) { - case kritor.event.RequestEvent.RequestType.FRIEND_APPLY: { - e = new KarinFriendApplyRequest({ self_id, time: data.time, content: data.friend_apply }) - e.raw_message = [ - '', - ].join(' ') - break - } - case kritor.event.RequestEvent.RequestType.GROUP_APPLY: { - e = new KarinGroupApplyRequest({ self_id, time: data.time, content: data.group_apply }) - e.raw_message = [ - '', - ].join(' ') - break - } - case kritor.event.RequestEvent.RequestType.GROUP_JOIN: { - e = new KarinInvitedJoinGroupRequest({ self_id, time: data.time, content: data.invited_group }) - e.raw_message = [ - '', - ].join(' ') - break - } - } - delete e.replyCallback - return e - } -} - -const Converters = { - messageConverter: new MessageConverter(), - noticeConverter: new NoticeConverter(), - requestConverter: new RequestConverter(), -} - -export default Converters diff --git a/lib/adapter/kritor/index.js b/lib/adapter/kritor/index.js deleted file mode 100644 index ac9c7a2..0000000 --- a/lib/adapter/kritor/index.js +++ /dev/null @@ -1,172 +0,0 @@ -import Api from './api.js' -import * as grpc from '@grpc/grpc-js' -import Converters from './converter.js' -import { proto, kritor } from 'kritor-proto' -import * as protoLoader from '@grpc/proto-loader' - -export default class Kritor { - #server - - /** - * @private - * @type {import('../../index.js').logger} - */ - #logger - - /** - * @private - * @type {import('../../index.js').common} - */ - #common - - /** - * @private - * @type {import('../../index.js').config} - */ - #config - - /** - * @private - * @type {import('../../index.js').listener} - */ - listener - constructor (logger, common, config, listener) { - this.dir = proto - this.#logger = logger - this.#common = common - this.#config = config - this.listener = listener - } - - /** - * 获取 proto 文件 - * @param {string} filename - * @param {string[]} dirs - * @returns {unknown} - */ - getProtoGrpcType (filename) { - filename = `${this.dir}/${filename}` - const dirs = [this.dir] - const definition = protoLoader.loadSync(filename, { - includeDirs: dirs, - keepCase: true, - longs: String, - // enums: String, - defaults: true, - oneofs: true, - }) - return grpc.loadPackageDefinition(definition) - } - - /** - * 初始化 gRPC 服务器 - */ - init () { - try { - const authenticationProtoGrpcType = this.getProtoGrpcType('auth/authentication.proto') - const coreProtoGrpcType = this.getProtoGrpcType('core/core.proto') - const customizationGrpcType = this.getProtoGrpcType('developer/customization.proto') - const developerGrpcType = this.getProtoGrpcType('developer/developer.proto') - const QsignGrpcType = this.getProtoGrpcType('developer/qsign.proto') - const eventProtoGrpcType = this.getProtoGrpcType('event/event.proto') - const GroupFileGrpcType = this.getProtoGrpcType('file/group_file.proto') - const friendGrpcType = this.getProtoGrpcType('friend/friend.proto') - const groupGrpcType = this.getProtoGrpcType('group/group.proto') - const guildGrpcType = this.getProtoGrpcType('guild/guild.proto') - const messageProtoGrpcType = this.getProtoGrpcType('message/message.proto') - const reverseProtoGrpcType = this.getProtoGrpcType('reverse/reverse.proto') - const webGrpcType = this.getProtoGrpcType('web/web.proto') - - const authenticationServer = { - /** - * - * @param {grpc.ServerReadableStream} call - * @constructor - */ - RegisterPassiveListener: (call) => { - const metadata = call.metadata.getMap() - const { 'kritor-self-uid': uid, 'kritor-self-uin': uin } = metadata - /** 监听上报事件 */ - call.on('data', data => { - this.#logger.debug('上报事件:', data) - // 事件分发according to data.type - switch (data.type) { - /** 消息事件 */ - case kritor.event.EventType.EVENT_TYPE_MESSAGE: { - const message = Converters.messageConverter.convert(data.message, uin || uid) - this.listener.emit('message', message) - break - } - /** 通知事件 */ - case kritor.event.EventType.EVENT_TYPE_NOTICE: { - const notice = Converters.noticeConverter.convert(data.notice, uin || uid) - this.listener.emit('notice', notice) - break - } - /** 请求事件 */ - case kritor.event.EventType.EVENT_TYPE_REQUEST: { - const request = Converters.requestConverter.convert(data.request, uin || uid) - this.listener.emit('request', request) - break - } - /** 元事件 */ - case kritor.event.EventType.EVENT_TYPE_CORE_EVENT: { - this.#logger.info('核心事件: ' + JSON.stringify(data.toJSON())) - } - } - }) - }, - ReverseStream: (call) => { - const metadata = call.metadata.getMap() - const { 'kritor-self-uid': uid, 'kritor-self-uin': uin } = metadata - this.#logger.info('收到反向流请求:', metadata) - - const bot = new Api(call, { uid, uin }, this.#logger, this.#common, this.#config, this.listener) - /** 注册bot */ - this.listener.emit('bot', { type: 'grpc', bot }) - }, - } - - this.#server = new grpc.Server() - this.#server.addService(coreProtoGrpcType.kritor.core.CoreService.service, authenticationServer) - this.#server.addService(authenticationProtoGrpcType.kritor.authentication.AuthenticationService.service, authenticationServer) - this.#server.addService(eventProtoGrpcType.kritor.event.EventService.service, authenticationServer) - this.#server.addService(reverseProtoGrpcType.kritor.reverse.ReverseService.service, authenticationServer) - this.#server.addService(messageProtoGrpcType.kritor.message.MessageService.service, authenticationServer) - this.#server.addService(friendGrpcType.kritor.friend.FriendService.service, authenticationServer) - this.#server.addService(groupGrpcType.kritor.group.GroupService.service, authenticationServer) - this.#server.addService(guildGrpcType.kritor.guild.GuildService.service, authenticationServer) - this.#server.addService(GroupFileGrpcType.kritor.file.GroupFileService.service, authenticationServer) - this.#server.addService(customizationGrpcType.kritor.customization.CustomizationService.service, authenticationServer) - this.#server.addService(developerGrpcType.kritor.developer.DeveloperService.service, authenticationServer) - this.#server.addService(QsignGrpcType.kritor.developer.QsignService.service, authenticationServer) - this.#server.addService(webGrpcType.kritor.web.WebService.service, authenticationServer) - - this.#server.bindAsync(this.#config.Server.grpc.host, grpc.ServerCredentials.createInsecure(), (err) => { - if (err) { - this.#logger.error('grpc服务器启动失败:', err) - } else { - this.#logger.info('[服务器][启动成功][grpc]: ', this.#logger.green(`http://${this.#config.Server.grpc.host}`)) - } - }) - - this.listener.once('restart.grpc', () => { - this.#logger.mark('[服务器][重启][grpc] 正在重启grpc服务器...') - this.#restart() - }) - - /** 关闭 gRPC 服务器 */ - this.listener.once('exit_grpc', () => this.#server.forceShutdown()) - } catch (error) { - this.#logger.error('初始化grpc服务器失败: ', error) - } - } - - /** 重启 gRPC 服务器 */ - async #restart () { - this.#server.forceShutdown() - /** 延迟1秒 */ - await this.#common.sleep(1000) - this.init() - } -} diff --git a/lib/adapter/onebot/OneBot11.js b/lib/adapter/onebot/OneBot11.js deleted file mode 100644 index e89f914..0000000 --- a/lib/adapter/onebot/OneBot11.js +++ /dev/null @@ -1,1461 +0,0 @@ -import { - KarinAtElement, - KarinContactElement, - KarinFaceElement, - KarinFileElement, - KarinForwardElement, - KarinImageElement, - KarinJsonElement, - KarinLocationElement, - KarinPokeElement, - KarinRecordElement, - KarinReplyElement, - KarinTextElement, - KarinvideoElement, - KarinXmlElement, -} from '../../bot/KarinElement.js' - -import { - KarinFriendRecallNotice, - KarinGroupPokeNotice, - KarinGroupRecallNotice, - KarinGroupMemberIncreasedNotice, - KarinGroupMemberDecreasedNotice, - KarinGroupAdminChangedNotice, - KarinGroupMemberBanNotice, - KarinGroupFileUploadedNotice, - KarinGroupMessagReactionNotice, -} from '../../bot/KarinNotice.js' - -import WebSocket from 'ws' -import { randomUUID } from 'crypto' -import { KarinAdapter } from '../../adapter/adapter.js' -import { KarinMessage } from '../../bot/KarinMessage.js' - -/** - * @typedef OneBotSegmentNode - * @property {'node'} type - 节点类型 - * @property {object} data - 节点数据 - * @property {string} data.uin - 用户QQ号 - * @property {string} data.name - 用户昵称 - * @property {Array} data.content - 节点内容 - */ - -/** - * @typedef {object} version 适配器版本信息 - * @property {string} version.app_name - 适配器名称 - * @property {string} version.version - 适配器版本 - */ - -/** - * @class OneBot11 - * @extends KarinAdapter - */ -export class OneBot11 extends KarinAdapter { - /** - * @private - * @type {import('../../index.js').logger} - */ - #logger - - /** - * @private - * @type {import('../../index.js').common} - */ - #common - - /** - * @private - * @type {import('../../index.js').listener} - */ - #listener - - /** - * @private - * @type {import('../../index.js').config} - */ - #config - - /** - * 是否初始化 - */ - #init = false - - constructor (logger, common, listener, config) { - super() - // 私有属性是为了避免后续更改这些属性消失而导致插件无法使用... - this.#logger = logger - this.#common = common - this.#listener = listener - this.#config = config - } - - /** - * 反向ws初始化 - */ - async server (socket, request) { - this.socket = socket - const { host, upgrade, 'x-self-id': self_id } = request.headers - const url = `ws://${host + request.url}` - this.account = { uid: self_id, uin: self_id, name: '' } - this.adapter.id = 'QQ' - this.adapter.name = 'onebot11' - this.adapter.type = 'websocket' - this.adapter.sub_type = 'server' - this.adapter.url = url - - this.logger('info', `[反向WS][onebot11-${upgrade}][${self_id}] ` + logger.green(url)) - /** 监听消息事件 */ - this.socket.on('message', (bot) => { - /** 防止空事件 */ - bot = bot.toString().trim() || '{"post_type":"error","error":"空事件"}' - this.logger('debug', `[收到事件]:${bot}`) - const data = JSON.parse(bot) - data.echo ? this.socket.emit(data.echo, data) : this.#init && this.#event(data) - }) - - /** 监听断开 */ - this.socket.on('close', () => this.logger('warn', `[反向WS] 连接断开:${url}`)) - await this.getSelf() - this.#init = true - } - - /** - * 正向ws初始化 - * @param {string} url - WebSocket连接地址 - */ - async client (url) { - /** 创建连接 */ - this.socket = new WebSocket(url) - - this.socket.on('open', async () => { - this.adapter.id = 'QQ' - this.adapter.name = 'onebot11' - this.adapter.type = 'websocket' - this.adapter.sub_type = 'client' - this.adapter.url = url - - logger.info('[正向WS][连接成功][onebot11] ' + logger.green(url)) - this.index = 0 - /** 监听消息事件 */ - this.socket.on('message', bot => { - /** 防止空事件 */ - bot = bot.toString().trim() || '{"post_type":"error","error":"空事件"}' - this.logger('debug', `[收到事件]:${bot}`) - const data = JSON.parse(bot) - data.echo ? this.socket.emit(data.echo, data) : this.#init && this.#event(data) - }) - - /** 监听断开 */ - this.socket.on('close', () => this.logger('warn', `[正向WS] 连接断开:${url}`)) - await this.getSelf() - this.#init = true - }) - - /** 监听错误 */ - this.socket.on('error', error => { - logger.debug('[正向WS] 发生错误', error) - this.socket.close() - }) - - /** 监听断开 */ - this.socket.on('close', async () => { - this.index++ - logger.warn(`[正向WS][重连次数:${this.index}] 连接断开,将在5秒后重连:${url}`) - /** 停止全部监听 */ - this.socket.removeAllListeners() - await this.#common.sleep(5000) - this.client(url) - }) - } - - /** - * 获取当前登录号信息 - */ - async getSelf () { - const data = await this.GetCurrentAccount() - try { - const { app_name, app_version: version } = await this.GetVersion() - this.version.name = app_name - this.version.app_name = app_name - this.version.version = version - } catch (e) { - /** 兼容onebots */ - const { app_name, app_version: version } = await this.SendApi('get_version') - this.version.name = app_name - this.version.app_name = app_name - this.version.version = version - } - - this.account.uid = data.account_uid - this.account.uin = data.account_uin - this.account.name = data.account_name - this.logger('info', `[加载完成][app_name:${this.version.name}][version:${this.version.version}] ` + logger.green(this.adapter.url)) - /** 注册bot */ - this.#listener.emit('bot', { type: 'websocket', bot: this }) - } - - /** 是否初始化 */ - get isInit () { - return new Promise((resolve) => { - const timer = setInterval(() => { - if (this.account.name) { - const { app_name, version } = this.version - this.logger('info', `建立连接成功:[${app_name}(${version})] ${this.adapter.url}`) - clearInterval(timer) - resolve(true) - } - }, 100) - }) - } - - /** 处理事件 */ - #event (data) { - switch (data.post_type) { - case 'meta_event': - this.#meta_event_event(data) - break - case 'message': - this.#message_event(data) - break - case 'notice': - this.#notice_event(data) - break - case 'request': - this.#request_event(data) - break - case 'message_sent': - this.#message_event(data) - break - default: - this.logger('info', `未知事件:${JSON.stringify(data)}`) - } - } - - /** 元事件 */ - #meta_event_event (data) { - switch (data.meta_event_type) { - case 'heartbeat': - this.logger('trace', `[心跳]:${JSON.stringify(data.status, null, 2)}`) - break - case 'lifecycle': - this.logger('debug', `[生命周期]:${{ enable: 'OneBot启用', disable: 'OneBot停用', connect: 'WebSocket连接成功' }[data.sub_type]}`) - break - } - - this.#listener.emit('meta_event', data) - } - - /** 消息事件 */ - async #message_event (data) { - const message = { - self_id: data.self_id, - user_id: data.sender.user_id + '', - time: data.time, - message_id: data.message_id, - message_seq: data.message_seq, - sender: { - uid: data.sender.user_id + '', - uin: data.sender.user_id + '', - nick: data.sender.nickname || '', - }, - } - - message.elements = this.AdapterConvertKarin(data.message) - - switch (data.message_type) { - case 'private': - message.contact = { - scene: 'friend', - peer: data.sender.user_id, - sub_peer: data.sender.user_id, - } - break - case 'group': - message.group_id = data.group_id - message.contact = { - scene: 'group', - peer: data.group_id, - sub_peer: data.sender.user_id, - } - break - } - - const e = new KarinMessage(message) - e.bot = this - /** - * 快速回复 开发者不应该使用这个方法,应该使用由karin封装过后的reply方法 - * @param {KarinElement[]} elements - 消息内容 - */ - e.replyCallback = async (elements) => { - if (data.message_type === 'private') { - return this.send_private_msg(data.user_id, elements) - } else { - return this.send_group_msg(data.group_id, elements) - } - } - - this.#listener.emit('message', e) - } - - /** 通知事件 */ - #notice_event (data) { - const time = data.time - const self_id = data.self_id - switch (data.notice_type) { - case 'group_upload': { - const content = { - group_id: data.group_id, - operator_uid: data.user_id, - operator_uin: data.user_id, - file_id: data.file.id, - file_sub_id: undefined, - file_name: data.file.name, - file_size: data.file.size, - expire_time: undefined, - biz: undefined, - url: undefined, - } - data = new KarinGroupFileUploadedNotice({ time, self_id, content }) - break - } - case 'group_admin': { - const content = { - group_id: data.group_id, - target_uid: data.user_id, - target_uin: data.user_id, - is_admin: data.sub_type === 'set', - } - data = new KarinGroupAdminChangedNotice({ time, self_id, content }) - break - } - case 'group_decrease': { - const content = { group_id: data.group_id } - switch (data.sub_type) { - // 主动退群 - case 'leave': - content.type = 0 - content.target_uid = data.user_id - content.target_uin = data.user_id - break - // 成员被踢 - case 'kick': - content.type = 1 - content.operator_uid = data.operator_id - content.operator_uin = data.operator_id - content.target_uid = data.user_id - content.target_uin = data.user_id - break - // bot被踢 - case 'kick_me': - content.type = 2 - content.operator_uid = data.operator_id - content.operator_uin = data.operator_id - break - } - data = new KarinGroupMemberDecreasedNotice({ time, self_id, content }) - break - } - case 'group_increase': { - const content = { - group_id: data.group_id, - operator_uid: data.operator_id, - operator_uin: data.operator_id, - target_uid: data.user_id, - target_uin: data.user_id, - type: data.sub_type === 'approve' ? 0 : 1, - } - data = new KarinGroupMemberIncreasedNotice({ time, self_id, content }) - break - } - case 'group_ban': { - const content = { - group_id: data.group_id, - operator_uid: data.operator_id, - operator_uin: data.operator_id, - target_uid: data.user_id, - target_uin: data.user_id, - duration: data.duration, - type: data.sub_type === 'ban' ? 1 : 0, - } - data = new KarinGroupMemberBanNotice({ time, self_id, content }) - break - } - case 'friend_add': - // todo - this.logger('info', `[好友添加]:${JSON.stringify(data)}`) - break - case 'group_recall': { - const content = { - group_id: data.group_id, - message_id: data.message_id, - operator_uid: data.operator_id, - operator_uin: data.operator_id, - target_uid: data.user_id, - target_uin: data.user_id, - message_seq: data.message_id, - tip_text: '', - } - data = new KarinGroupRecallNotice({ time, self_id, content }) - break - } - case 'friend_recall': { - const content = { - operator_uid: data.operator_id, - operator_uin: data.operator_id, - message_id: data.message_id, - tip_text: undefined, - } - data = new KarinFriendRecallNotice({ time, self_id, content }) - break - } - case 'notify': - switch (data.sub_type) { - case 'poke': { - const content = { - group_id: data.group_id, - operator_uid: data.operator_id, - operator_uin: data.operator_id, - target_uid: data.user_id, - target_uin: data.user_id, - action: undefined, - suffix: undefined, - action_image: undefined, - } - data = new KarinGroupPokeNotice({ time, self_id, content }) - break - } - case 'lucky_king': - this.logger('info', `[运气王]:${JSON.stringify(data)}`) - break - case 'honor': - this.logger('info', `[群荣誉变更]:${JSON.stringify(data)}`) - break - } - break - case 'group_msg_emoji_like': { - const content = { - group_id: data.group_id, - operator_uid: data.user_id, - operator_uin: data.user_id, - message_id: data.message_id, - face_id: data.likes[0].emoji_id, - // llob目前只有上报点击 没有取消 暂时默认为true - is_emoji: true, - } - data = new KarinGroupMessagReactionNotice({ time, self_id, content }) - break - } - default: { - return this.#logger.error('未知通知事件:', JSON.stringify(data)) - } - } - - this.#listener.emit('notice', data) - } - - /** 请求事件 */ - #request_event (data) { - switch (data.request_type) { - case 'friend': - this.logger('info', `[加好友请求]:${JSON.stringify(data)}`) - /** - * 快速操作 - * @param {boolean} approve 是否同意请求 - * @param {string} remark 添加后的好友备注(仅在同意时有效) - */ - data.approve = async (approve, remark) => { - const obj = { flag: data.flag, approve, remark } - if (!remark) delete obj.remark - return await this.SendApi('set_friend_add_request', obj) - } - break - case 'group': - this.logger('info', `[加群请求]:${JSON.stringify(data)}`) - /** - * 快速操作 - * @param {boolean} approve 是否同意请求/邀请 - * @param {string} remark 拒绝理由(仅在拒绝时有效) - */ - data.approve = async (approve, remark) => { - const { flag, sub_type } = data - const obj = { flag, sub_type, approve, remark } - if (!remark) delete obj.remark - return await this.SendApi('set_group_add_request', obj) - } - break - } - // Bot.emit('request', data) - } - - /** - * onebot11转karin - * @param {Array<{type: string, data: any}>} data onebot11格式消息 - * @return {Array} karin格式消息 - * */ - AdapterConvertKarin (data) { - const elements = [] - for (const i of data) { - switch (i.type) { - case 'text': - elements.push(new KarinTextElement(i.data.text)) - break - case 'face': - elements.push(new KarinFaceElement(i.data.id)) - break - case 'image': - elements.push(new KarinImageElement(i.data.url || i.data.file)) - break - case 'record': - elements.push(new KarinRecordElement(i.data.url || i.data.file)) - break - case 'video': - elements.push(new KarinvideoElement(i.data.url || i.data.file)) - break - case 'at': - elements.push(new KarinAtElement(i.data.qq)) - break - case 'poke': - elements.push(new KarinPokeElement(i.data.type, i.data.id)) - break - case 'contact': - elements.push(new KarinContactElement(i.data.type, i.data.id)) - break - case 'location': - elements.push(new KarinLocationElement(i.data.lat, i.data.lon, i.data.title, i.data.content)) - break - case 'music': - // 收不到这种类型的消息,收到的都是json - break - case 'reply': - elements.push(new KarinReplyElement(i.data.id)) - break - case 'forward': - elements.push(new KarinForwardElement(i.data.id)) - break - case 'node': - // 收不到这种类型的消息,收到的都是forward - break - case 'file': - elements.push(new KarinFileElement(i.data.name, i.data.url)) - break - case 'json': - elements.push(new KarinJsonElement(i.data.data)) - break - case 'xml': - elements.push(new KarinXmlElement(i.data.data)) - break - case 'custom': - case 'button': - case 'rows': - default: { - elements.push(new KarinTextElement(JSON.stringify(i))) - } - } - } - return elements - } - - /** - * karin转onebot11 - * @param {Array} data karin格式消息 - * @return {Array<{type: string, data: any}>} onebot11格式消息 - * */ - KarinConvertAdapter (data) { - const elements = [] - const selfUin = this.account.uin + '' - const selfNick = this.account.name - - for (const i of data) { - switch (i.type) { - case 'text': - elements.push({ type: 'text', data: { text: i.text } }) - break - case 'face': - elements.push({ type: 'face', data: { id: i.id } }) - break - case 'at': - elements.push({ type: 'at', data: { qq: String(i.uid || i.uin) } }) - break - case 'reply': - elements.push({ type: 'reply', data: { id: i.message_id } }) - break - case 'image': - case 'video': - case 'file': { - elements.push({ type: i.type, data: { file: i.file } }) - break - } - case 'xml': - case 'json': { - elements.push({ type: i.type, data: { data: i.data } }) - break - } - case 'node': { - let { id, type, user_id, nickname, content } = i - if (id) { - elements.push({ type, data: { id } }) - break - } - content = this.KarinConvertAdapter(content) - elements.push({ type, data: { uin: user_id || selfUin, name: nickname || selfNick, content } }) - break - } - case 'forward': { - elements.push({ type: 'forward', data: { id: i.id } }) - break - } - case 'voice': { - elements.push({ type: 'record', data: { file: i.file, magic: i.magic || false } }) - break - } - case 'music': { - if (i.platform) { - elements.push({ type: 'music', data: { type: i.platform, id: i.id } }) - } else { - const { url, audio, title, content, image } = i - elements.push({ type: 'music', data: { type: 'custom', url, audio, title, content, image } }) - } - break - } - case 'button': { - elements.push({ type: 'button', data: { buttons: i.buttons } }) - break - } - case 'markdown': { - const { type, ...data } = i - elements.push({ type, data: { ...data } }) - break - } - case 'rows': { - for (const val of i.rows) { - elements.push({ type: 'button', data: { buttons: val.buttons } }) - } - break - } - case 'poke': { - elements.push({ type: 'poke', data: { type: i.poke, id: i.id } }) - break - } - case 'long_msg': { - elements.push({ type: 'long_msg', data: { id: i.id } }) - break - } - default: { - elements.push(i) - logger.info(i) - } - } - } - return elements - } - - /** - * 构建特定账号的日志 - * @param {string} level - 日志等级 - * @param {string} args - 日志内容 - */ - logger (level, ...args) { - this.#logger.bot(level, this.account.uid || this.account.uin, ...args) - } - - /** - * 获取头像url - * @param {number} size 头像大小,默认`0` - * @param {string} uid 用户qq,默认为机器人QQ - * @returns {string} 头像的url地址 - */ - getAvatarUrl (uid = this.account.uid || this.account.uin, size = 0) { - return Number(uid) ? `https://q1.qlogo.cn/g?b=qq&s=${size}&nk=${uid}` : `https://q.qlogo.cn/qqapp/${uid}/${uid}/${size}` - } - - /** - * 获取群头像 - * @param {string} group_id - 群号 - * @param {number} [size] - 头像大小,默认`0` - * @param {number} [history] - 历史头像记录,默认`0`,若要获取历史群头像则填写1,2,3... - * @returns {string} - 群头像的url地址 - */ - getGroupAvatar (group_id, size = 0, history = 0) { - return `https://p.qlogo.cn/gh/${group_id}/${group_id}${history ? '_' + history : ''}/` + size - } - - /** - * 对事件执行快速操作 隐藏api - * @param {object} context 事件数据对象,可做精简,如去掉 message 等无用字段 - * @param {object} operation 快速操作对象,例如 {"ban": true, "reply": "请不要说脏话"} - */ - async #handle_quick_operation (context, operation) { - if (!operation.remark) delete operation.remark - return await this.SendApi('.handle_quick_operation', { context, operation }) - } - - /** - * 发送私聊消息 - * @param {number} user_id - 用户ID - * @param {Array} message - 要发送的内容 - * @returns {Promise<{message_id:string}>} - 消息ID - */ - async send_private_msg (user_id, message) { - const obMessage = this.KarinConvertAdapter(message) - // this.logger(`${logger.green(`Send private ${user_id}: `)}${this.logSend(message)}`)) - return await this.SendApi('send_private_msg', { user_id, message: obMessage }) - } - - /** - * 发送群消息 - * @param {number} group_id - 群号 - * @param {Array} message - 要发送的内容 - * @returns {Promise<{message_id:string}>} - 消息ID - */ - async send_group_msg (group_id, message) { - const obMessages = this.KarinConvertAdapter(message) - return await this.SendApi('send_group_msg', { group_id, message: obMessages }) - } - - /** - * 发送消息 - * - * @param {KarinContact} contact - * @param {Array} elements - * @returns {Promise<{message_id:string}>} - 消息ID - */ - async SendMessage (contact, elements) { - let { scene, peer } = contact - const message_type = scene === 'group' ? 'group' : 'private' - scene = scene === 'group' ? 'group_id' : 'user_id' - const message = this.KarinConvertAdapter(elements) - const params = { [scene]: peer, message_type, message } - return await this.SendApi('send_msg', params) - } - - /** - * 上传合并转发消息 - * @param {KarinContact} contact - 联系人信息 - * @param {KarinNodeElement[] | KarinNodeElement} elements - nodes - * @returns {Promise} - 资源id - * */ - async UploadForwardMessage (contact, elements) { - if (!Array.isArray(elements)) elements = [elements] - if (elements.some(element => element.type !== 'node')) { - throw new Error('elements should be all node type') - } - const { scene, peer } = contact - const message_type = scene === 'group' ? 'group_id' : 'user_id' - const messages = this.KarinConvertAdapter(elements) - - const params = { [message_type]: String(peer), messages } - return await this.SendApi('send_forward_msg', params) - } - - /** - * 撤回消息 - * @param {null} [_contact] - ob11无需提供contact参数 - * @param {number} message_id - 消息ID - * @returns {Promise} - */ - async RecallMessage (_contact, message_id) { - return await this.SendApi('delete_msg', { message_id }) - } - - /** - * 获取消息 - * @param {null} [_contact] - ob11无需提供contact参数 - * @param {number} message_id - 消息ID - * @returns {Promise} - 消息内容 - */ - - async GetMessage (_contact, message_id) { - let res = await this.SendApi('get_msg', { message_id }) - res = { - time: res.time, - message_id: res.message_id, - message_seq: res.message_id, - contact: { - scene: res.message_type === 'group' ? 'group' : 'private', - peer: res.sender.user_id, // 拿不到group_id... - }, - sender: { - uid: res.sender.user_id, - uin: res.sender.user_id, - nick: res.sender.nickname, - }, - elements: this.AdapterConvertKarin(res.message), - } - return res - } - - /** 获取msg_id获取历史消息 */ - async GetHistoryMessage (contact, start_message_id, count) { - const type = contact.scene === 'group' ? 'group_id' : 'user_id' - const param = { [type]: contact.peer, message_id: start_message_id, count } - const api = contact.scene === 'group' ? 'get_group_msg_history' : 'get_friend_msg_history' - const res = await this.SendApi(api, param, 120) - const ret = [] - for (const i of res.messages) { - let { time = Date.now(), message_id, message_seq = message_id, sender, message } = i - const { user_id, nickname } = sender - sender = { uid: user_id, uin: user_id, nick: nickname } - const elements = this.AdapterConvertKarin(message) - ret.push({ time, message_id, message_seq, contact, sender, elements }) - } - return ret - } - - /** - * todo - * 获取合并转发消息 - * @param {string} id - 合并转发 ID - * @returns {Promise} - 消息内容,使用消息的数组格式表示,数组中的消息段全部为 node 消息段 - */ - async get_forward_msg (id) { - return await this.SendApi('get_forward_msg', { id }) - } - - /** - * 发送好友赞 - * @param {{ - * target_uid?: string, - * target_uin?: string, - * times: number - * }} options - * @param options.target_uid - 好友 QQ 号 任选其一 - * @param options.target_uin - 好友 QQ 号 任选其一 - * @param options.times - 赞的次数,默认为 10 - */ - async VoteUser (options) { - const user_id = Number(options.target_uid || options.target_uin) - await this.SendApi('send_like', { user_id, times: options.times || 10 }) - } - - /** - * 群组踢人 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * reject_add_request?:boolean, - * kick_reason?:string - * }} options - * @param options.group_id - 群组ID - * @param options.target_uid - 被踢出目标的 uid 任选其一 - * @param options.target_uin - 被踢出目标的 uin 任选其一 - * @param options.reject_add_request - 是否拒绝再次申请,默认为false - * @param options.kick_reason - 踢出原因,可选 - * @returns {Promise} - 踢出操作的响应 - */ - async KickMember (options) { - const { group_id, target_uid, target_uin, reject_add_request } = options - const user_id = Number(target_uid || target_uin) - await this.SendApi('set_group_kick', { group_id, user_id, reject_add_request }) - } - - /** - * 禁言用户 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * duration:number - * }} options - * @param options.group_id - 群组ID - * @param options.target_uid - 被禁言目标的uin 任选其一 - * @param options.target_uin - 被禁言目标的uid 任选其一 - * @param options.duration - 禁言时长(单位:秒) - * @returns {Promise} - 禁言操作的响应 - */ - async BanMember (options) { - const { group_id, target_uid, target_uin, duration } = options - const user_id = Number(target_uid || target_uin) - await this.SendApi('set_group_ban', { group_id, user_id, duration }) - } - - /** - * 群组匿名用户禁言 - * @param {number} group_id - 群号 - * @param {object} anonymous - 要禁言的匿名用户对象(群消息上报的 anonymous 字段) - * @param {string} [anonymous_flag] - 要禁言的匿名用户的 flag(需从群消息上报的数据中获得) - * @param {number} [duration=1800] - 禁言时长,单位秒,无法取消匿名用户禁言 - */ - async set_group_anonymous_ban (group_id, anonymous, anonymous_flag, duration = 1800) { - await this.SendApi('set_group_anonymous_ban', { group_id, anonymous, anonymous_flag, duration }) - } - - /** - * 群组全员禁言 - * @param {number} group_id - 群号 - * @param {boolean} [enable=true] - 是否全员禁言 - */ - async SetGroupWholeBan (group_id, enable = true) { - await this.SendApi('set_group_whole_ban', { group_id, enable }) - } - - /** - * 设置群管理员 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * is_admin:boolean - * }} options - 设置管理员选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 要设置为管理员的用户uid - * @param options.target_uin - 要设置为管理员的用户uin - * @param options.is_admin - 是否设置为管理员 - * @returns {Promise} - 设置群管理员操作的响应 - */ - async SetGroupAdmin (options) { - const { group_id, target_uid, target_uin, is_admin } = options - const user_id = Number(target_uid || target_uin) - await this.SendApi('set_group_admin', { group_id, user_id, enable: is_admin }) - } - - /** - * 群组匿名 - * @param {number} group_id - 群号 - * @param {boolean} [enable=true] - 是否允许匿名聊天 - */ - async set_group_anonymous (group_id, enable = true) { - await this.SendApi('set_group_anonymous', { group_id, enable }) - } - - /** - * 修改群名片 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * card:string - * }} options - * @param {number} options.group_id - 群组ID - * @param {string|number} options.target_uid - 目标用户的 uid 任选其一 - * @param {string|number} options.target_uin - 目标用户的 uin 任选其一 - * @param {string} options.card - 新的群名片 - * @returns {Promise} - 修改群名片操作的响应 - */ - async ModifyMemberCard (options) { - const { group_id, target_uid, target_uin, card } = options - const user_id = Number(target_uid || target_uin) - await this.SendApi('set_group_card', { group_id, user_id, card }) - } - - /** - * 设置群名 - * @param {number} group_id - 群号 - * @param {string} group_name - 新群名 - */ - async ModifyGroupName (group_id, group_name) { - await this.SendApi('set_group_name', { group_id, group_name }) - } - - /** - * 退出群组 - * @param {number} group_id - 群号 - * @param {boolean} [is_dismiss=false] - 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 - */ - async LeaveGroup (group_id, is_dismiss = false) { - await this.SendApi('set_group_leave', { group_id, is_dismiss }) - } - - /** - * 设置群专属头衔 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * unique_title:string - * }} options - 设置头衔选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 目标用户的uid - * @param options.target_uin - 目标用户的uin - * @param options.unique_title - 新的群头衔 - * @returns {Promise} - 设置群头衔操作的响应 - */ - async SetGroupUniqueTitle (options) { - const { group_id, target_uid, target_uin, unique_title } = options - const user_id = Number(target_uid || target_uin) - const special_title = unique_title - const duration = -1 - await this.SendApi('set_group_special_title', { group_id, user_id, special_title, duration }) - } - - /** - * 处理加好友请求 - * @param {string} flag - 加好友请求的 flag(需从上报的数据中获得) - * @param {boolean} [approve=true] - 是否同意请求 - * @param {string} [remark=''] - 添加后的好友备注(仅在同意时有效) - */ - async set_friend_add_request (flag, approve = true, remark = '') { - await this.SendApi('set_friend_add_request', { flag, approve, remark }) - } - - /** - * 处理加群请求/邀请 - * @param {string} flag - 加群请求的 flag(需从上报的数据中获得) - * @param {string} sub_type - add 或 invite,请求类型(需要和上报消息中的 sub_type 字段相符) - * @param {boolean} [approve=true] - 是否同意请求/邀请 - * @param {string} [reason=''] - 拒绝理由(仅在拒绝时有效) - */ - async set_group_add_request (flag, sub_type, approve = true, reason = '') { - await this.SendApi('set_group_add_request', { flag, sub_type, approve, reason }) - } - - /** - * 获取登录号信息 - * @returns {Promise<{account_uid:string, account_uin:string, account_name:string}>} - 登录号信息 - */ - async GetCurrentAccount () { - const res = await this.SendApi('get_login_info') - return { - account_uid: res.user_id, - account_uin: res.user_id, - account_name: res.nickname, - } - } - - /** - * 获取陌生人信息 不支持批量获取 - * @param {Object} [options] - 陌生人信息选项 - * @param {Array} [options.target_uids] - 目标用户的 uid 数组 可选 - * @param {Array} [options.target_uins] - 目标用户的 uin 数组 可选 - * @param {boolean} [options.no_cache=false] - 是否不使用缓存 - * @returns {Promise} - 获取到的陌生人信息 - */ - async GetStrangerProfileCard (options) { - const { target_uids = [], target_uins = [], no_cache = false } = options || {} - const user_id = target_uids.length > 0 ? target_uids[0] : target_uins[0] - const res = await this.SendApi('get_stranger_info', { user_id, no_cache }) - return [res] - } - - /** - * 获取好友列表 - * @returns {Promise>} - 好友列表 - */ - async GetFriendList () { - /** @type {{ - * user_id: number, - * user_name: string?, - * user_remark: string, - * remark: string?, - * nickname: string?, - }[]} **/ - const friendList = await this.SendApi('get_friend_list') - return friendList.map(friend => { - return { - uin: friend.user_id, - uid: friend.user_id, - qid: '', - nick: friend.nickname || friend.user_name, - remark: friend.remark || friend.user_remark, - } - }) - } - - /** - * 获取群信息 - * @param {number} group_id - 群号 - * @param {boolean} [no_cache=false] - 是否不使用缓存 - * @returns {Promise} - 群信息 - */ - async GetGroupInfo (group_id, no_cache = false) { - /** - * @type {{ - * group_id: number, - * group_name: string, - * group_memo: string, - * group_remark: string, - * group_create_time: number, - * group_level: number, - * member_count: number, - * max_member_count: number, - * admins: number[] - * }} - */ - const groupInfo = await this.SendApi('get_group_info', { group_id, no_cache }) - return { - group_id: groupInfo.group_id, - group_name: groupInfo.group_name, - group_remark: groupInfo.group_memo || groupInfo.group_remark, - max_member_count: groupInfo.max_member_count, - member_count: groupInfo.member_count, - group_uin: groupInfo.group_id, - admins: groupInfo.admins, - } - } - - /** - * 获取群列表 - * @returns {Promise>} - 群列表 - */ - async GetGroupList () { - const groupList = await this.SendApi('get_group_list') - return groupList?.map(groupInfo => { - return { - group_id: groupInfo.group_id, - group_name: groupInfo.group_name, - group_remark: groupInfo.group_memo || groupInfo.group_remark, - max_member_count: groupInfo.max_member_count, - member_count: groupInfo.member_count, - group_uin: groupInfo.group_id, - admins: groupInfo.admins, - } - }) - } - - /** - * 获取群成员信息 - * @param {{ - * group_id:string, - * target_uid?:string, - * target_uin?:string, - * refresh?:boolean - * }} options - 获取成员信息选项 - * @param options.group_id - 群组ID - * @param options.target_uid - 目标用户的uid - * @param options.target_uin - 目标用户的uin - * @param options.refresh - 是否刷新缓存,默认为 false - * @returns {Promise} - 获取群成员信息操作的响应 - */ - async GetGroupMemberInfo (options) { - const { group_id, target_uid, target_uin, refresh = false } = options - const user_id = Number(target_uid || target_uin) - /** - * @type {{ - * group_id: number, - * user_id: number, - * nickname: string, - * card: string, - * sex: string, - * age: number, - * area: string, - * join_time: number, - * last_sent_time: number, - * level: string, - * role: 'owner' | 'admin' | 'member', - * unfriendly: boolean, - * title: string, - * title_expire_time: number, - * card_changeable: boolean, - * shut_up_timestamp: number - * }} - */ - const groupMemberInfo = await this.SendApi('get_group_member_info', { group_id, user_id, no_cache: refresh }) - let level = 0 - try { - level = parseInt(groupMemberInfo.level) - } catch (e) { } - return { - uid: groupMemberInfo.user_id, - uin: groupMemberInfo.user_id, - nick: groupMemberInfo.nickname, - age: groupMemberInfo.age, - unique_title: groupMemberInfo.title, - unique_title_expire_time: groupMemberInfo.title_expire_time, - card: groupMemberInfo.card, - join_time: groupMemberInfo.join_time, - last_active_time: groupMemberInfo.last_sent_time, - level, - shut_up_timestamp: groupMemberInfo.shut_up_timestamp, - unfriendly: groupMemberInfo.unfriendly, - card_changeable: groupMemberInfo.card_changeable, - } - } - - /** - * 获取群成员列表 - * @param {Object} options - 获取成员列表选项 - * @param {number} options.group_id - 群组ID - * @param {boolean} [options.refresh] - 是否刷新缓存 - * @returns {Promise} - 获取群成员列表操作的响应 - */ - async GetGroupMemberList (options) { - const { group_id } = options - const gl = await this.SendApi('get_group_member_list', { group_id }) - return gl.map(groupMemberInfo => { - let level = 0 - try { - level = parseInt(groupMemberInfo.level) - } catch (e) { } - return { - uid: groupMemberInfo.user_id, - uin: groupMemberInfo.user_id, - nick: groupMemberInfo.nickname, - age: groupMemberInfo.age, - unique_title: groupMemberInfo.title, - unique_title_expire_time: groupMemberInfo.title_expire_time, - card: groupMemberInfo.card, - join_time: groupMemberInfo.join_time, - last_active_time: groupMemberInfo.last_sent_time, - level, - shut_up_timestamp: groupMemberInfo.shut_up_timestamp, - unfriendly: groupMemberInfo.unfriendly, - card_changeable: groupMemberInfo.card_changeable, - } - }) - } - - /** - * 获取群荣誉信息 - * @param {Object} options - 获取群荣誉信息选项 - * @param {number} options.group_id - 群号 - * @param {boolean} [options.refresh] - 是否刷新缓存 - * @returns {Promise} - 获取群荣誉信息操作的响应 - */ - async GetGroupHonor (options) { - const { group_id } = options - /** - * @typedef {{user_id: number, nickname: string, avatar: string, description: string}} GroupHonor - */ - /** - * @type {{ - * group_id: number, - * current_talkative: {user_id: number, nickname: string, avatar: string, day_count: number}, - * talkative_list: Array, - * performer_list: Array, - * legend_list: Array, - * strong_newbie_list: Array, - * emotion_list: Array, - * }} - */ - const groupHonor = await this.SendApi('get_group_honor_info', { group_id, type: 'all' }) - - const result = [] - groupHonor.talkative_list.forEach(honor => { - result.push({ - uin: honor.user_id, - uid: honor.user_id, - nick: honor.nickname, - honor_name: '历史龙王', - avatar: honor.avatar, - description: honor.description, - }) - }) - groupHonor.performer_list.forEach(honor => { - result.push({ - uin: honor.user_id, - uid: honor.user_id, - nick: honor.nickname, - honor_name: '群聊之火', - avatar: honor.avatar, - description: honor.description, - }) - }) - groupHonor.legend_list.forEach(honor => { - result.push({ - uin: honor.user_id, - uid: honor.user_id, - nick: honor.nickname, - honor_name: '群聊炽焰', - avatar: honor.avatar, - description: honor.description, - }) - }) - groupHonor.strong_newbie_list.forEach(honor => { - result.push({ - uin: honor.user_id, - uid: honor.user_id, - nick: honor.nickname, - honor_name: '冒尖小春笋', - avatar: honor.avatar, - description: honor.description, - }) - }) - groupHonor.emotion_list.forEach(honor => { - result.push({ - uin: honor.user_id, - uid: honor.user_id, - nick: honor.nickname, - honor_name: '快乐之源', - avatar: honor.avatar, - description: honor.description, - }) - }) - return result - } - - /** - * 对消息进行表情回应 - * @param {KarinContact} Contact - 联系人信息 - * @param {string} message_id - 消息ID - * @param {string} face_id - 表情ID - */ - async ReactMessageWithEmojiRequest (Contact, message_id, face_id, is_set = true) { - return await this.SendApi('set_msg_emoji_like', { message_id, emoji_id: face_id, is_set }) - } - - /** - * 上传群文件 - * @param {string} group_id - 群号 - * @param {string} file - 本地文件绝对路径 - * @param {string} name - 文件名称 必须提供 - * @param {string} [folder] - 父目录ID 不提供则上传到根目录 - */ - async UploadGroupFile (group_id, file, name) { - return await this.SendApi('upload_group_file', { group_id, file, name }) - } - - /** - * 上传私聊文件 - * @param {string} user_id - 用户ID - * @param {string} file - 本地文件绝对路径 - * @param {string} name - 文件名称 必须提供 - */ - async UploadPrivateFile (user_id, file, name) { - return await this.SendApi('upload_private_file', { user_id, file, name }) - } - - /** - * 获取 Cookies - * @param {string} domain - 需要获取 cookies 的域名 - * @returns {Promise} - Cookies - */ - async get_cookies (domain) { - return (await this.SendApi('get_cookies', { domain }))?.cookies - } - - /** - * 获取 CSRF Token - * @returns {Promise} - CSRF Token - */ - async get_csrf_token () { - return (await this.SendApi('get_csrf_token')).token - } - - /** - * 获取 QQ 相关接口凭证 - * @param {string} domain - 需要获取 cookies 的域名 - * @returns {Promise<{ - * cookies: string, - * cstf_token: number - * }>} - QQ 相关接口凭证 - */ - async get_credentials (domain) { - return await this.SendApi('get_credentials', { domain }) - } - - /** - * 获取语音 - * @param {string} file - 收到的语音文件名 - * @param {string} out_format - 要转换到的格式 - * @returns {Promise<{ - * file: string, - * out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' - * }>} - 转换后的语音文件路径 - */ - async get_record (file, out_format) { - return await this.SendApi('get_record', { file, out_format }) - } - - /** - * 获取图片 - * @param {string} file - 收到的图片文件名 - * @returns {Promise<{ - * size: number, - * filename: string, - * url: string - * }>} - 下载后的图片文件路径 - */ - async get_image (file) { - return await this.SendApi('get_image', { file }) - } - - /** - * 检查是否可以发送图片 - * @returns {Promise} - 是否可以发送图片 - */ - async can_send_image () { - return (await this.SendApi('can_send_image'))?.yes - } - - /** - * 检查是否可以发送语音 - * @returns {Promise} - 是否可以发送语音 - */ - async can_send_record () { - return (await this.SendApi('can_send_record'))?.yes - } - - /** - * 获取运行状态 - * @returns {Promise} - 运行状态 - */ - async get_status () { - return await this.SendApi('get_status') - } - - /** - * 获取版本信息 - * @returns {Promise} - 版本信息 - */ - async GetVersion () { - return await this.SendApi('get_version_info') - } - - /** - * 重启 OneBot 实现 - * @param {number} [delay=0] - 要延迟的毫秒数 - * @returns {Promise} - 重启状态 - */ - async set_restart (delay = 0) { - return await this.SendApi('set_restart', { delay }) - } - - /** - * 清理缓存 - * @returns {Promise} - 清理状态 - */ - async clean_cache () { - return await this.SendApi('clean_cache') - } - - /** - * 发送API请求 - * @param {string} action - API断点 - * @param {object} params - API参数 - * @returns {Promise} - API返回 - */ - async SendApi (action, params = {}, time) { - if (!time) time = this.#config.timeout('ws') - const echo = randomUUID() - const request = JSON.stringify({ echo, action, params }) - logger.debug(`[API请求] ${action}: ${request}`) - return new Promise((resolve, reject) => { - this.socket.send(request) - this.socket.once(echo, (data) => { - if (data.status === 'ok') { - resolve(data.data) - } else { - this.logger('error', `[Api请求错误] ${action}: ${JSON.stringify(data, null, 2)}`) - reject(data) - } - }) - /** 设置一个超时计时器 */ - setTimeout(() => { - reject(new Error('API请求超时')) - }, time * 1000) - }) - } - - /** - * karin的node转为ob的node - * @param {{type: 'node', user_id: string, nickname: string, content: object[]}} node - * @return OneBotSegmentNode - */ - formatNode (node) { - return { - type: 'node', - data: { - uin: node.user_id, - name: node.nickname, - content: node.content, - }, - } - } -} - -export default { - type: 'websocket', - path: '/onebot/v11/ws', - adapter: OneBot11, -} diff --git a/lib/bot/KarinElement.js b/lib/bot/KarinElement.js deleted file mode 100644 index fa567ae..0000000 --- a/lib/bot/KarinElement.js +++ /dev/null @@ -1,920 +0,0 @@ -/** - * ~~消息类型.待补充~~ 已基本实现大部分消息类型 - * @typedef {'text' | 'image' | 'at' | 'file' | 'face' | 'reply' | 'xml' | 'json' | 'unknown' | 'location' | 'voice' | 'video' | 'contact' | 'poke' |'music' | String} KarinElementType - */ - -export class KarinElement { - /** - * @type {KarinElementType} - */ - type -} - -/** - * @extends KarinElement - */ -export class KarinTextElement extends KarinElement { - /** - * 构建一个文本元素 - * @param {string} text - 文本内容 - */ - constructor (text) { - super() - this.type = 'text' - /** - * @type {string} text - */ - this.text = text - } -} - -/** - * @extends KarinElement - */ -export class KarinFaceElement extends KarinElement { - /** - * 构建一个表情元素 - * @param {number} id - 表情id - * @param {boolean?} is_big - 是否为大表情 - */ - constructor (id, is_big = false) { - super() - this.type = 'face' - /** - * @type {Number} id - */ - this.id = id - /** - * @type {boolean} is_big - */ - this.is_big = is_big - } - - /** - * @type {Number} - */ - id - - /** - * @type {boolean} - */ - is_big -} - -/** - * @extends KarinElement - */ -export class KarinImageElement extends KarinElement { - /** - * 构建一个图片元素 - * @param {string|Buffer} file - 图片文件 - * @param {any} [args] - */ - constructor (file, ...args) { - super() - this.type = 'image' - /** - * @type {string|Buffer} - */ - this.file = file - if (args) Object.assign(this, ...args) - } - - /** - * @type {string|Buffer} - */ - file -} - -/** - * @extends KarinElement - */ -export class KarinRecordElement extends KarinElement { - /** - * 构建一个语音元素 - * @param {string} file - 语音文件 - * @param {any} [args] - */ - constructor (file, ...args) { - super() - this.type = 'voice' - /** - * @type {string} - */ - this.file = file - if (args) Object.assign(this, ...args) - } - - /** - * @type {string} - */ - file -} - -/** - * @extends KarinElement - */ -export class KarinvideoElement extends KarinElement { - /** - * 构建一个视频元素 - * @param {string} file - 视频文件 - * @param {any} [args] - */ - constructor (file, ...args) { - super() - this.type = 'video' - /** - * @type {string} - */ - this.file = file - if (args) Object.assign(this, ...args) - } - - /** - * @type {string} - */ - file -} - -/** - * @extends KarinElement - */ -export class KarinAtElement extends KarinElement { - /** - * 构建一个at元素 - * @param {String} uid - 被at的uid 优先使用uid - * @param {String} [uin] - 被at的uin - * @param {String} [name] - 被at的昵称 - */ - constructor (uid, uin, name) { - super() - this.type = 'at' - /** - * @type {String} - */ - this.uid = String(uid) - /** - * @type {String} - */ - this.uin = uin - /** - * @type {String} 被at的昵称 - */ - this.name = name - } - - /** - * @type {String} - */ - uid - - /** - * @type {Number|String} - */ - uin -} - -/** - * @extends KarinElement - */ -export class KarinPokeElement extends KarinElement { - /** - * 构建一个戳一戳元素 - * @param {number} poke - 戳一戳类型 - * @param {number} id - 戳一戳对象id - * @param {number} strength - 戳一戳强度 - */ - constructor (poke, id, strength = 1) { - super() - this.type = 'poke' - /** - * @type {number} - */ - this.poke = poke - /** - * @type {number} - */ - this.id = id - /** - * @type {number} - */ - this.strength = strength - } - - /** - * @type {number} - */ - poke - - /** - * @type {number} - */ - id - /** - * @type {number} - */ - strength -} - -/** - * @extends KarinElement - */ -export class KarinContactElement extends KarinElement { - /** - * 构建一个联系人元素 - * @param {string} platform - 类型,'qq' 表示推荐好友,'group' 表示推荐群 - * @param {string} id - 被推荐人的 QQ 号或被推荐群的群号 - */ - constructor (platform, id) { - super() - this.type = 'contact' - /** - * @type {string} 类型,'qq' 表示推荐好友,'group' 表示推荐群 - */ - this.platform = platform - /** - * @type {string} 被推荐人的 QQ 号或被推荐群的群号 - */ - this.id = id - } - - /** - * @type {string} 类型,'qq' 表示推荐好友,'group' 表示推荐群 - */ - platform - - /** - * @type {string} 被推荐人的 QQ 号或被推荐群的群号 - */ - id -} - -/** - * @extends KarinElement - */ -export class KarinLocationElement extends KarinElement { - /** - * 构建一个位置元素 - * @param {number} lat - 纬度 - * @param {number} lon - 经度 - * @param {string} title - 位置名称 - * @param {string} content - 位置描述 - */ - constructor (lat, lon, title, content) { - super() - this.type = 'location' - /** - * @type {number} 纬度 - */ - this.lat = lat - /** - * @type {number} 经度 - */ - this.lon = lon - /** - * @type {string} 位置名称 - */ - this.title = title - /** - * @type {string} 位置描述 - */ - this.content = content - } - - /** - * @type {number} 纬度 - */ - lat - - /** - * @type {number} 经度 - */ - lon - - /** - * @type {string} 位置名称 - */ - title - - /** - * @type {string} 位置描述 - */ - content -} - -/** - * @extends KarinElement - */ -export class KarinMusicElement extends KarinElement { - /** - * 构建一个音乐元素 - * @param {'qq' | '163' | 'xm'} platform - 平台 - * @param {string} id - 音乐id - */ - constructor (platform, id) { - super() - this.type = 'music' - /** - * @type {string} 平台 - */ - this.platform = platform - /** - * @type {string} 音乐id - */ - this.id = id - } - - /** - * @type {string} 平台 - */ - platform - - /** - * @type {string} 音乐id - */ - id -} - -/** - * @extends KarinElement - */ -export class KarinCustomMusicElement extends KarinElement { - /** - * 构建自定义音乐分享 - * @param {string} url - 点击后跳转目标 URL - * @param {string} audio - 音乐 URL - * @param {string} title - 标题 - * @param {string} [content] - 内容描述 - * @param {string} [image] - 图片 URL - * @returns {<{type: string, url: string, audio: string, title: string, content: string, image: string}>} 自定义音乐分享消息 - */ - constructor (url, audio, title, content, image) { - super() - this.type = 'music' - /** - * @type {string} 点击后跳转目标 URL - */ - this.url = url - /** - * @type {string} 音乐 URL - */ - this.audio = audio - /** - * @type {string} 标题 - */ - this.title = title - /** - * @type {string} 内容描述 - */ - this.content = content - /** - * @type {string} 图片 URL - */ - this.image = image - } - - /** - * @type {string} 点击后跳转目标 URL - */ - url - - /** - * @type {string} 音乐 URL - */ - audio - - /** - * @type {string} 标题 - */ - title - - /** - * @type {string} 内容描述 - */ - content - - /** - * @type {string} 图片 URL - */ - image -} - -/** - * @extends KarinElement - */ -export class KarinReplyElement extends KarinElement { - /** - * 构建一个回复元素 - * @param {String} message_id - 被回复的消息id - */ - constructor (message_id) { - super() - this.type = 'reply' - /** - * @type {String} message_id - */ - this.message_id = String(message_id) - } - - /** - * @type {String} message_id - */ - message_id -} - -/** - * @extends KarinElement - */ -export class KarinForwardElement extends KarinElement { - /** - * 构建一个转发元素 - * @param {String} id - 转发的消息id - */ - constructor (id, ...args) { - super() - this.type = 'forward' - /** - * @type {String} id - */ - this.id = id - if (args) Object.assign(this, ...args) - } - - /** - * @type {String} id - */ - id -} - -/** - * @extends KarinElement - */ -export class KarinNodeElement extends KarinElement { - /** - * 构建一个node元素 - * @param {string} user_id - 用户id - * @param {string} nickname - 用户昵称 - * @param {KarinElement[]} content - 内容 - * @returns {KarinNodeElement} - */ - constructor (user_id, nickname, content) { - super() - this.type = 'node' - /** - * @type {string} user_id - */ - this.user_id = user_id - /** - * @type {string} nickname - */ - this.nickname = nickname - /** - * @type {string} content - */ - this.content = content - } - - /** - * @type {string} user_id - */ - user_id - - /** - * @type {string} nickname - */ - nickname - - /** - * @type {string} content - */ - content -} - -/** - * @extends KarinElement - */ -export class KarinXmlElement extends KarinElement { - /** - * 构建一个xml元素 - * @param {String} data - xml内容 - * @returns {KarinXmlElement} - */ - constructor (data) { - super() - this.type = 'xml' - /** - * @type {String} xml - */ - this.data = data - } - - /** - * @type {String} data - */ - data -} - -/** - * @extends KarinElement - */ -export class KarinJsonElement extends KarinElement { - /** - * 构建一个json元素 - * @param {String} data - json内容 - * @returns {KarinJsonElement} - */ - constructor (data) { - super() - this.type = 'json' - /** - * @type {String} json - */ - this.data = data - } - - /** - * @type {String} data - */ - data -} - -/** - * @extends KarinElement - */ -export class KarinFileElement extends KarinElement { - /** - * 构建一个文件元素 - * @param {string} file - 文件信息 - * @param {String} [name] - 文件名 - * @param {Number} [size] - 文件大小 - * @param {Number} [expire_time] - 过期时间 - * @param {String} [id] - 文件id - * @param {Number} [biz] - 文件biz - * @param {String} [sub_id] - 文件sub_id - * @returns {KarinFileElement} - */ - constructor (file, name, size, expire_time, id, biz, sub_id) { - super() - this.type = 'file' - /** - * @type {string} file - */ - this.file = file - /** - * @type {String} name - */ - this.name = name - /** - * @type {Number} size - */ - this.size = size - /** - * @type {Number} expire_time - */ - this.expire_time = expire_time - /** - * @type {String} id - */ - this.id = id - /** - * @type {Number} biz - */ - this.biz = biz - /** - * @type {String} sub_id - */ - this.sub_id = sub_id - } - - /** - * @type {string} file - */ - file - - /** - * @type {String} name - */ - name - - /** - * @type {Number} size - */ - size - - /** - * @type {Number} expire_time - */ - expire_time - - /** - * @type {String} id - */ - id - - /** - * @type {Number} biz - */ - biz - - /** - * @type {String} sub_id - */ - sub_id -} - -/** - * @extends KarinElement - */ -export class KarinBubbleFaceElement extends KarinElement { - /** - * 构建一个气泡表情元素 - * @param {number} id - 表情id - * @param {number} count - 表情数量 - */ - constructor (id, count) { - super() - this.type = 'bubble_face' - /** - * @type {number} id - */ - this.id = id - /** - * @type {number} count - */ - this.count = count - } - - /** - * @type {number} id - */ - id - - /** - * @type {number} count - */ - count -} - -/** - * @extends KarinElement - */ -export class KarinBasketballElement extends KarinElement { - /** - * 构建一个篮球元素 - * @param {number} id - 篮球id - */ - constructor (id) { - super() - this.type = 'basketball' - /** - * @type {number} id - */ - this.id = id - } - - /** - * @type {number} id - */ - id -} - -/** - * @extends KarinElement - */ -export class KarinDiceElement extends KarinElement { - /** - * 构建一个骰子元素 - * @param {number} id - 骰子id - */ - constructor (id) { - super() - this.type = 'dice' - /** - * @type {number} id - */ - this.id = id - } - - /** - * @type {number} id - */ - id -} - -/** - * @extends KarinElement - */ -export class KarinRpsElement extends KarinElement { - /** - * 构建一个猜拳元素 - * @param {number} id - 猜拳id - */ - constructor (id) { - super() - this.type = 'rps' - /** - * @type {number} id - */ - this.id = id - } - - /** - * @type {number} id - */ - id -} - -/** - * @extends KarinElement - */ -export class KarinWeatherElement extends KarinElement { - /** - * 构建一个天气元素 - * @param {string} city - 城市名 - * @param {string} code - 城市代码 - */ - constructor (city, code) { - super() - this.type = 'weather' - /** - * @type {string} city - */ - this.city = city - /** - * @type {string} code - */ - this.code = code - } - - /** - * @type {string} city - */ - city - - /** - * @type {string} code - */ - code -} - -/** - * @extends KarinElement - */ -export class KarinShareElement extends KarinElement { - /** - * 构建一个链接分享 - * @param {string} url - 链接 - * @param {string} title - 标题 - * @param {string} content - 内容 - * @param {string} image - 图片 - * @returns {KarinShareElement} - */ - constructor (url, title, content, image) { - super() - this.type = 'share' - /** - * @type {string} url - */ - this.url = url - /** - * @type {string} title - */ - this.title = title - /** - * @type {string} content - */ - this.content = content - /** - * @type {string} image - */ - this.image = image - } - - /** - * @type {string} url - */ - url - - /** - * @type {string} title - */ - title - - /** - * @type {string} content - */ - content - - /** - * @type {string} image - */ - image -} - -/** - * @extends KarinElement - */ -export class KarinGiftElement extends KarinElement { - /** - * 不支持发送 - * 构建一个礼物元素 - * @param {number} qq - 发送者qq - * @param {number} id - 礼物id - */ - constructor (qq, id) { - super() - this.type = 'gift' - /** - * @type {number} id - */ - this.id = id - /** - * @type {number} qq - */ - this.qq = qq - } - - /** - * @type {number} id - */ - id - - /** - * @type {number} qq - */ - qq -} - -/** - * @extends KarinElement - */ -export class KarinLongMsgElement extends KarinElement { - /** - * 构建一个长消息元素 - * @param {string} id - id - */ - constructor (id) { - super() - this.type = 'long_msg' - /** - * @type {string} id - */ - this.id = id - } - - /** - * @type {string} id - */ - id -} - -/** - * Contact - * @class KarinContact - */ -export class KarinContact { - /** - * @param {'group'|'friend'|'guild'|'nearby'|'stranger'|'stranger_from_group'} scene - 场景 - * @param {string} peer - 场景对应ID - * @param {string?} sub_peer - 场景子ID - */ - constructor (scene, peer, sub_peer) { - this.scene = scene - this.peer = peer - this.sub_peer = sub_peer - } - - /** - * @type {'group'|'friend'|'guild'|'nearby'|'stranger'|'stranger_from_group'} - 场景名称 - */ - scene - - /** - * @type {string} - 场景对应ID - */ - peer - - /** - * @type {string | undefined} - 场景子ID - */ - sub_peer - - /** - * group - * @param {string|number} peer - * @return {KarinContact} - */ - static group (peer) { - return new KarinContact('group', peer + '') - } - - /** - * private - * @param {string|number} peer - * @param {string?} sub_peer - * @return {KarinContact} - */ - static private (peer, sub_peer) { - return new KarinContact('friend', peer + '', sub_peer ? sub_peer + '' : undefined) - } -} diff --git a/lib/bot/KarinEvent.js b/lib/bot/KarinEvent.js deleted file mode 100644 index edf3fd1..0000000 --- a/lib/bot/KarinEvent.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * @typedef {} KarinNoticeType - */ - -export class KarinEvent { - /** - * 构造一个event - * @param {{ - * event: 'message'|'request'|'notice', - * self_id: string, - * user_id: string, - * group_id?: string, - * time: number, - * contact: { - * scene: 'group'|'friend'|'guild'|'nearby'|'stranger'|'stranger_from_group', - * peer: string, - * sub_peer?: string - * }, - * sender: { - * uid: string, - * uin: string, - * nick?: string - * }, - * sub_event: 'group'|'friend'|'guild'|'nearby'|'stranger'|'stranger_from_group'|'friend_poke' | 'friend_recall' | 'friend_file_come' | 'group_poke' | 'group_card_changed' | 'group_member_unique_title_changed' | 'group_essence_changed' | 'group_recall' | 'group_member_increase' | 'group_member_decrease' | 'group_admin_changed' | 'group_member_banned' | 'group_sign' | 'group_whole_ban' | 'group_file_uploaded' - * }} params - */ - constructor ({ event, self_id, user_id, group_id = '', time, contact, sender, sub_event }) { - this.self_id = self_id - this.user_id = user_id - this.group_id = group_id - this.time = time - this.event = event - this.contact = contact - this.sender = sender - this.sub_event = sub_event - } - - /** - * 机器人id - * @type {string} - */ - self_id - - /** - * 用户id - * @type {string} - */ - user_id - - /** - * 群id - * @type {string} - */ - group_id - - /** - * 事件类型 - * @type {'message'|'notice'|'request'} - */ - event - - /** - * 消息时间戳 - * @type {number} - */ - time - - /** - * 联系人信息 - * @type {KarinContact} - */ - contact - - /** - * 发送者信息 - * @type {{ - * uid: string, - * uin: string, - * nick?: string - * }} - */ - sender - - /** - * 事件类型 - * @type {'group'|'friend'|'guild'|'nearby'|'stranger'|'stranger_from_group'|'friend_poke' | 'friend_recall' | 'friend_file_come' | 'group_poke' | 'group_card_changed' | 'group_member_unique_title_changed' | 'group_essence_changed' | 'group_recall' | 'group_member_increase' | 'group_member_decrease' | 'group_admin_changed' | 'group_member_banned' | 'group_sign' | 'group_whole_ban' | 'group_file_uploaded'} - */ - sub_event - - /** - * 是否为主人 - * @type {boolean} - */ - isMaster - - /** - * 是否为管理员 机器人子权限 - * @type {boolean} - */ - isAdmin - - /** - * 是否为私聊 - * @type {boolean} - */ - isPrivate - - /** - * 是否为群聊 - * @type {boolean} - */ - isGroup - - /** - * 是否为频道 - * @type {boolean} - */ - isGuild - - /** - * 是否为群临时会话 - * @type {boolean} - */ - isGroupTemp - - /** - * 日志函数字符串 - * @type {string} - */ - logFnc - - /** - * 日志用户字符串 - * @type {string} - */ - logText - - /** - * 存储器 由开发者自行调用 - * @type {Map} - */ - store - - /** - * 回复函数 这个方法由适配器实现,开发者不应该直接调用 - * @type {ReplyCallback} - */ - replyCallback - - /** - * 回复函数 由karin对replyCallback封装过后的快速回复方法 - * @type {QuickReplyCallback} - */ - reply - - /** - * bot实现 - * @type {KarinAdapter} - */ - bot -} - -/** - * @typedef {function(Array, number): Promise<{ message_id?: string }>} ReplyCallback - * @param {Array} elements - 发送的消息元素 只能是数组 - * @param {number} retry_count - 重试次数 - * @returns {Promise<{ message_id?: string }>} - 返回消息ID - */ - -/** - * @typedef {function(string|KarinElement|Array, {at: boolean, reply: boolean, recallMsg: number, button: boolean, retry_count: number}): Promise<{ message_id?: string }>} QuickReplyCallback - * @param {string|KarinElement|Array} elements - 发送的消息 - * @param {object} [options] - 回复数据 - * @param {boolean} [options.at] - 是否at用户 - * @param {boolean} [options.reply] - 是否引用回复 - * @param {number} [options.recallMsg] - 群聊是否撤回消息,0-120秒,0不撤回 - * @param {boolean} [options.button] - 是否使用按钮 - * @param {number} [options.retry_count] - 重试次数 - * @returns {Promise<{ message_id?: string }>} - 返回消息ID - */ diff --git a/lib/bot/KarinMessage.js b/lib/bot/KarinMessage.js deleted file mode 100644 index 5969f9d..0000000 --- a/lib/bot/KarinMessage.js +++ /dev/null @@ -1,144 +0,0 @@ -import { KarinEvent } from './KarinEvent.js' - -/** - * @class KarinMessage - */ -export class KarinMessage extends KarinEvent { - /** - * 构造一个消息 - * @param {{ - * self_id: string, - * user_id: string, - * group_id?: string, - * time: number, - * message_id: string, - * message_seq: string, - * raw_message: string, - * contact: { - * scene: 'group'|'friend'|'guild'|'group_temp'|'stranger'|'stranger_group', - * peer: string, - * sub_peer?: string - * }, - * sender: { - * uid: string, - * uin: string, - * nick?: string - * }, - * elements: Array, - * msg?: string, - * game?: string, - * image?: Array, - * at?: string, - * atBot?: boolean, - * atAll?: boolean, - * file?: object, - * reply_id?: string - * }} params - */ - constructor ({ - self_id, - user_id, - time, - message_id, - message_seq, - raw_message = '', - contact, - sender, - elements, - group_id = '', - msg = '', - game = '', - image = [], - at = [], - atBot = false, - atAll = false, - file = {}, - reply_id = '' - }) { - super({ event: 'message', self_id, user_id, group_id, time, contact, sender, sub_event: contact.scene }) - this.message_id = message_id - this.message_seq = message_seq - this.raw_message = raw_message - this.elements = elements - this.msg = msg - this.game = game - this.image = image - this.at = at - this.atBot = atBot - this.atAll = atAll - this.file = file - this.reply_id = reply_id - } - - /** - * 消息id - * @type {string} - */ - message_id - - /** - * 消息序列号 - * @type {string} 消息序列号 - */ - message_seq - - /** - * 适合人类阅读的消息体 - * @type {string} 适合人类阅读的消息体 - */ - raw_message - - /** - * 消息体元素 - * @type {Array} - */ - elements - - /** - * 框架处理后的文本 - * @type {string} - */ - msg - - /** - * 游戏类型 - * @type {string} - */ - game - - /** - * 图片数组 - * @type {Array} - */ - image - - /** - * at - * @type {string} - */ - at - - /** - * atBot - * @type {boolean} - */ - atBot - - /** - * atAll - * @type {boolean} - */ - atAll - - /** - * 文件元素 - * @type {object} - */ - file - - /** - * 引用消息id - * @type {string} - */ - reply_id -} diff --git a/lib/bot/KarinNotice.js b/lib/bot/KarinNotice.js deleted file mode 100644 index 2185ad5..0000000 --- a/lib/bot/KarinNotice.js +++ /dev/null @@ -1,1009 +0,0 @@ -/** - * todo 本文件下可能要与type.js转移到同一个地方 - */ - -/** - * 通知事件子类型 - * @typedef {'friend_poke' | 'friend_recall' | 'friend_file_come' | 'group_poke' | 'group_card_changed' | 'group_member_unique_title_changed' | 'group_essence_changed' | 'group_recall' | 'group_member_increase' | 'group_member_decrease' | 'group_admin_change' | 'group_member_ban' | 'group_sign_in' | 'group_whole_ban' | 'group_file_uploaded'} KarinNoticeType - * @typedef {} - */ - -import { KarinEvent } from './KarinEvent.js' -import { Contacts, SendersNotice } from './UserInfo.js' - -/** - * @class KarinNotice - */ -export class KarinNotice extends KarinEvent { - /** - * 构造一个通知 - * @param {{ - * self_id: string, - * user_id: string, - * group_id?: string, - * time: number, - * message_id: string, - * message_seq: string, - * raw_message: string, - * contact: { - * scene: 'group'|'friend', - * peer: string, - * sub_peer?: string - * }, - * sender: { - * operator_uid?: string, - * operator_uin?: string, - * target_uid?: string, - * target_uin?: string - * }, - * content: any, - * sub_event: 'friend_poke' | 'friend_recall' | 'friend_file_come' | 'group_poke' | 'group_card_changed' | 'group_member_unique_title_changed' | 'group_essence_changed' | 'group_recall' | 'group_member_increase' | 'group_member_decrease' | 'group_admin_change' | 'group_member_ban' | 'group_sign_in' | 'group_whole_ban' | 'group_file_uploaded' - * }} - */ - constructor ({ self_id, sub_event, time, contact, sender, user_id, message_id, message_seq, raw_message = '', group_id = '' }) { - super({ event: 'notice', self_id, user_id, group_id, time, contact, sender, sub_event }) - this.message_id = message_id - this.message_seq = message_seq - this.raw_message = raw_message - } - - /** - * 消息id - * @type {string} - */ - message_id - - /** - * 消息序列号 - * @type {string} - */ - message_seq - - /** - * 适合人类阅读的消息体 - * @type {string} - */ - raw_message - - /** - * 对应事件的结构体 - * @type {any} - */ - content -} - -/** - * 好友头像戳一戳 - */ -export class KarinFriendPokeNotice extends KarinNotice { - sub_event = 'friend_poke' - - /** - * @param {{ - * self_id: string, - * content: { - * operator_uid: string, - * operator_uin: String, - * action: String, - * suffix: String, - * action_image: String - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('friend', operator_uid) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'friend_poke', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * operator_uid: string, - * operator_uin: String, - * action: String, - * suffix: String, - * action_image: String - * }} - */ - content -} - -/** - * 好友消息撤回 - */ -export class KarinFriendRecallNotice extends KarinNotice { - sub_event = 'friend_recall' - - /** - * @param {{ - * self_id: string, - * content: { - * operator_uid: string, - * operator_uin: String, - * message_id: String, - * tip_text: String - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('friend', operator_uid) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'friend_recall', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - } - super(data) - this.content = content - } - - /** - * @type {{ - * operator_uid: string, - * operator_uin: String, - * message_id: String, - * tip_text: String - * }} - */ - content -} - -/** - * 私聊文件上传 - */ -export class KarinFriendFileUploadedNotice extends KarinNotice { - sub_event = 'friend_file_uploaded' - - /** - * @param {{ - * self_id: string, - * content: { - * operator_uid: string, - * operator_uin: String, - * file_id: String, - * file_sub_id: String, - * file_name: String, - * file_size: Number, - * expire_time: Number, - * url: String - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('friend', operator_uid) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'friend_file_uploaded', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - } - super(data) - this.content = content - } - - /** - * @type {{ - * operator_uid: string, - * operator_uin: String, - * file_id: String, - * file_sub_id: String, - * file_name: String, - * file_size: Number, - * expire_time: Number, - * url: String - * }} - */ - content -} - -/** - * 群头像戳一戳 - */ -export class KarinGroupPokeNotice extends KarinNotice { - sub_event = 'group_poke' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * action: string, - * suffix: string, - * action_image: string - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_poke', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * action: string, - * suffix: string, - * action_image: string - * }} - */ - content -} - -/** - * 群名片改变 - */ -export class KarinGoupCardChangedNotice extends KarinNotice { - sub_event = 'group_card_changed' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * new_card: string - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_card_changed', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * new_card: string - * }} - */ - content -} - -/** - * 群成员专属头衔改变 - */ -export class KarinGroupUniqueTitleChangedNotice extends KarinNotice { - sub_event = 'group_member_unique_title_changed' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * title: string, - * target: string - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_member_unique_title_changed', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * title: string, - * target: string - * }} - */ - content -} - -/** - * 群精华消息改变 - */ -export class KarinGroupEssenceMessageNotice extends KarinNotice { - sub_event = 'group_essence_changed' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * message_id: string, - * sub_event: Number - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_essence_changed', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * message_id: string, - * sub_event: Number - * }} - */ - content -} - -/** - * 群撤回通知 - */ -export class KarinGroupRecallNotice extends KarinNotice { - sub_event = 'group_recall' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: String, - * message_id: String, - * tip_text: String, - * operator_uid: String, - * operator_uin: String, - * target_uid: String, - * target_uin: String, - * message_seq: String - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_recall', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: Number, - * message_id: String, - * tip_text: String, - * operator_uid: String, - * operator_uin: String, - * target_uid: String, - * target_uin: String, - * message_seq: String - * }} - */ - content -} - -/** - * 群成员增加 - */ -export class KarinGroupMemberIncreasedNotice extends KarinNotice { - sub_event = 'group_member_increase' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * type: 0|1 - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_member_increase', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * new_card: 0|1 - * }} - */ - content -} - -/** - * 群成员减少 - */ -export class KarinGroupMemberDecreasedNotice extends KarinNotice { - sub_event = 'group_member_decrease' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid?: string, - * operator_uin?: string, - * target_uid?: string, - * target_uin: string, - * type: 0|1|2 - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_member_decrease', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid?: string, - * operator_uin: string, - * target_uid?: string, - * target_uin: string, - * type: 0|1|2 - * }} - */ - content -} - -/** - * 群管理员变动 - */ -export class KarinGroupAdminChangedNotice extends KarinNotice { - sub_event = 'group_admin_change' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * target_uid: string, - * target_uin: string, - * is_admin: boolean - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = { target_uid, target_uin } - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_admin_change', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: target_uid || target_uin, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * target_uid: string, - * target_uin: string, - * is_admin: boolean - * }} - */ - content -} - -/** - * 群成员被禁言 - */ -export class KarinGroupMemberBanNotice extends KarinNotice { - sub_event = 'group_member_ban' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * duration: Number, - * type: 0|1 - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_member_ban', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * duration: Number, - * type: 0|1 - * }} - */ - content -} - -/** - * 群签到 - */ -export class KarinGroupSignInNotice extends KarinNotice { - sub_event = 'group_sign_in' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * target_uid: string, - * target_uin: string, - * action: string, - * rank_image: string - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_sign_in', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: target_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * new_card: string, - * suffix: string, - * action_image: string - * }} - */ - content -} - -/** - * 群全员禁言 - */ -export class KarinGroupWholeBanNotice extends KarinNotice { - sub_event = 'group_whole_ban' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * is_ban: boolean - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_whole_ban', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * is_ban: boolean - * }} - */ - content -} - -/** - * 群文件上传 - */ -export class KarinGroupFileUploadedNotice extends KarinNotice { - sub_event = 'group_file_uploaded' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * file_id: string, - * file_sub_id: string, - * file_name: string, - * file_size: Number, - * expire_time: Number, - * biz: Number, - * url: string - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_file_uploaded', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * target_uid: string, - * target_uin: string, - * new_card: string, - * suffix: string, - * action_image: string, - * file_id: string, - * file_sub_id: string, - * file_name: string, - * file_size: Number, - * expire_time: Number, - * biz: Number, - * url: string - * }} - */ - content -} - -/** - * 群消息被表情回应 - */ -export class KarinGroupMessagReactionNotice extends KarinNotice { - sub_event = 'group_message_reaction' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * message_id: string, - * face_id: number, - * is_set: boolean - * }, - * time: Number - * }} - */ - constructor ({ self_id, content, time }) { - const { operator_uid = '', operator_uin = '', target_uid = '', target_uin = '', message_id = '', message_seq = '', group_id = '', raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = SendersNotice(operator_uid, operator_uin, target_uid, target_uin) - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_message_reaction', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: operator_uid, - message_id, - message_seq, - raw_message, - group_id, - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * operator_uid: string, - * operator_uin: string, - * message_id: string, - * face_id: number, - * is_set: boolean - * }} - */ - content -} diff --git a/lib/bot/KarinRequest.js b/lib/bot/KarinRequest.js deleted file mode 100644 index 7d518bb..0000000 --- a/lib/bot/KarinRequest.js +++ /dev/null @@ -1,215 +0,0 @@ -import { KarinEvent } from './KarinEvent.js' -import { Contacts } from './UserInfo.js' - -/** - * @class KarinRequest - */ -export class KarinRequest extends KarinEvent { - /** - * 构造一个请求事件 - * @param {{ - * self_id: string, - * user_id: string, - * group_id?: string, - * time: number, - * raw_message: string, - * contact: { - * scene: 'group'|'friend'|'guild'|'nearby'|'stranger'|'stranger_from_group', - * peer: string, - * sub_peer?: string - * }, - * sender: { - * applier_uid?: string, - * applier_uin?: string, - * inviter_uid?: string, - * inviter_uin?: string - * }, - * content: any, - * sub_event: 'friend_apply' | 'group_apply' | 'invited_group' - * }} params - */ - constructor ({ self_id, sub_event, time, contact, sender, user_id, raw_message = '', group_id = '' }) { - super({ event: 'request', self_id, user_id, group_id, time, contact, sender, sub_event }) - this.raw_message = raw_message - } - - /** - * 适合人类阅读的消息体 - * @type {string} - */ - raw_message - - /** - * 对应事件的结构体 - * @type {any} - */ - content -} - -/** - * 新的好友请求 - */ -export class KarinFriendApplyRequest extends KarinRequest { - sub_event = 'friend_apply' - - /** - * @param {{ - * self_id: string, - * content: { - * applier_uid: string, - * applier_uin: string, - * message: string - * }, - * time: Number - * }} params - */ - constructor ({ self_id, content, time }) { - const { applier_uid, applier_uin, raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('friend', applier_uid) - /** 构建发送者信息 */ - const sender = { - applier_uid, - applier_uin - } - - /** 构建通知 */ - const data = { - self_id, - sub_event: 'friend_apply', - time, - contact, - sender, - content, - // user_id与peer统一使用uid - user_id: applier_uid, - raw_message - } - super(data) - this.content = content - } - - /** - * @type {{ - * applier_uid: string, - * applier_uin: string, - * message: string - * }} - */ - content -} - -/** - * 新的加群请求 - */ -export class KarinGroupApplyRequest extends KarinRequest { - sub_event = 'group_apply' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * applier_uid: string, - * applier_uin: string, - * inviter_uid: string, - * inviter_uin: string, - * reason: string - * }, - * time: Number - * }} params - */ - constructor ({ self_id, content, time }) { - const { group_id, applier_uid, applier_uin, inviter_uid, inviter_uin, raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = { - applier_uid, - applier_uin, - inviter_uid, - inviter_uin - } - - /** 构建通知 */ - const data = { - self_id, - sub_event: 'group_apply', - time, - contact, - sender, - content, - // user_id与peer统一使用uid 这里指申请者 - user_id: applier_uid, - raw_message - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * applier_uid: string, - * applier_uin: string, - * inviter_uid: string, - * inviter_uin: string, - * reason: string - * }} - */ - content -} - -/** - * 收到邀请加群请求 - */ -export class KarinInvitedJoinGroupRequest extends KarinRequest { - sub_event = 'invited_group' - - /** - * @param {{ - * self_id: string, - * content: { - * group_id: string, - * inviter_uid: string, - * inviter_uin: string - * }, - * time: Number - * }} params - */ - constructor ({ self_id, content, time }) { - const { group_id, inviter_uid, inviter_uin, raw_message = '' } = content - /** 构建联系人 */ - const contact = Contacts('group', group_id) - /** 构建发送者信息 */ - const sender = { - inviter_uid, - inviter_uin - } - - /** 构建通知 */ - const data = { - self_id, - sub_event: 'invited_group', - time, - contact, - sender, - content, - // user_id与peer统一使用uid 这里指申请者 - user_id: inviter_uid, - raw_message - } - super(data) - this.content = content - } - - /** - * @type {{ - * group_id: string, - * inviter_uid: string, - * inviter_uin: string, - * reason: string - * }} - */ - content -} diff --git a/lib/bot/UserInfo.js b/lib/bot/UserInfo.js deleted file mode 100644 index c09e500..0000000 --- a/lib/bot/UserInfo.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 通知事件联系人 - * @param {string} scene - 场景 - * @param {string} peer - 群号或用户id - * @param {string} sub_peer - 子群号或用户id - * @returns {Contact} - * @typedef {Object} Contact - * @property {string} scene - 场景 - * @property {string} peer - 群号或用户id - * @property {string} sub_peer - 子群号或用户id - */ -export function Contacts (scene, peer, sub_peer = '') { - return { scene, peer, sub_peer } -} - -/** - * 发送者 - * @param {string} operator_uid - 操作者uid - * @param {string} operator_uin - 操作者uin - * @param {string} target_uid - 目标者uid - * @param {string} target_uin - 目标者uin - * @returns {Object} - */ -export function SendersNotice (operator_uid = '', operator_uin = '', target_uid = '', target_uin = '') { - return { operator_uid, operator_uin, target_uid, target_uin } -} diff --git a/lib/common/common.js b/lib/common/common.js deleted file mode 100644 index b147cec..0000000 --- a/lib/common/common.js +++ /dev/null @@ -1,416 +0,0 @@ -import fs from 'fs' -import path from 'path' -import axios from 'axios' -import { promisify } from 'util' -import { pipeline, Readable } from 'stream' - -/** - * 常用方法 - */ -export default class Common { - /** - * 日志模块 - * @type {import('../index.js').logger} - */ - #logger - /** - * 日志模块 - * @type {import('../index.js').segment} - */ - #segment - - /** - * 常用方法 - * @param {import('../index.js').logger} logger - 日志模块 - * @param {import('../index.js').segment} segment - 消息体模块 - */ - constructor (logger, segment) { - this.#logger = logger - this.#segment = segment - this.streamPipeline = promisify(pipeline) - } - - /** - * 休眠函数 - * @param ms 毫秒 - */ - sleep (ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) - } - - /** - * 下载保存文件 - * @param {string} fileUrl 下载地址 - * @param {string} savePath 保存路径 - * @param {import('axios').AxiosRequestConfig} [param] axios参数 - */ - async downFile (fileUrl, savePath, param = {}) { - try { - this.mkdir(path.dirname(savePath)) - this.#logger && this.#logger.debug(`[下载文件] ${fileUrl}`) - const response = await axios.get(fileUrl, { ...param, responseType: 'stream' }) - await this.streamPipeline(response.data, fs.createWriteStream(savePath)) - return true - } catch (err) { - this.#logger && this.#logger.error(`下载文件错误:${err}`) - return false - } - } - - /** - * 递归创建目录 - * @param {string} dirname - 要创建的文件夹路径 - */ - mkdir (dirname) { - if (fs.existsSync(dirname)) return true - /** 递归自调用 */ - if (this.mkdir(path.dirname(dirname))) fs.mkdirSync(dirname) - return true - } - - /** - * 标准化文件路径 - * @param {string} file - 相对路径 - * @param {boolean} [isDir] - 返回绝对路径 - * @param {boolean} [isFile] - 添加file://前缀 - * @returns {string} - 标准化后的路径 - */ - absPath (file, isDir = true, isFile = false) { - file = file.replace(/\\/g, '/') - if (file.startsWith('file://')) { - /** linux */ - if (path.sep === '/') { - file = file.replace('file://', '') - } else { - /** windows */ - file = file.replace(/^file:[/]{2,3}/g, '') - } - } - - file = path.normalize(file) - - /** 判断路径是否为绝对路径 否则转为绝对路径 */ - if (isDir && !path.isAbsolute(file)) { - file = path.resolve(file) - } - if (isFile) file = 'file://' + file - file = file.replace(/\\/g, '/') - return file - } - - /** - * 判断是否为文件夹 - * @param {string} path - 路径 - * @returns {boolean} - 返回true为文件夹 - */ - isDir (path) { - try { - return fs.statSync(path).isDirectory() - } catch { - return false - } - } - - /** - * 判断路径是否存在 - * @param {string} path - 路径 - * @returns {boolean} - 返回true为存在 - */ - exists (path) { - try { - return fs.existsSync(path) - } catch { - return false - } - } - - /** - * 将文件转换为不带前缀的base64字符串 - * @param {string|Buffer|http|stream.Readable} file - 文件路径或Buffer对象、可读流对象、http地址、base64://字符串 - * @param {object} options - 附加数据 - * @param {boolean} options.http - 为true时,http地址会直接返回,否则会下载文件并转换为base64字符串 - * @returns {Promise} - 返回base64字符串 - */ - async base64 (file, options = { http: false }) { - /** 先判断是否非字符串情况 */ - if (typeof file !== 'string') { - /** buffer */ - if (Buffer.isBuffer(file)) { - return file.toString('base64') - } - /** 可读流 */ - if (file instanceof Readable) { - const data_1 = await this.stream(file) - return data_1.toString('base64') - } - /** 未知类型 */ - throw new Error('未知类型') - } - - /** base64:// */ - if (file.startsWith('base64://')) { - return file.replace('base64://', '') - } - - /** url */ - if (file.startsWith('http://') || file.startsWith('https://')) { - if (options.http) return file - const response = await axios.get(file, { responseType: 'arraybuffer' }) - return Buffer.from(response.data, 'binary').toString('base64') - } - - /** file:/// */ - if (fs.existsSync(file.replace(/^file:\/\/\//, ''))) { - file = file.replace(/^file:\/\/\//, '') - return fs.readFileSync(file).toString('base64') - } - - /** file:// */ - if (fs.existsSync(file.replace(/^file:\/\//, ''))) { - file = file.replace(/^file:\/\//, '') - return fs.readFileSync(file).toString('base64') - } - - /** 无前缀base64:// */ - const buffer = Buffer.from(file, 'base64').toString('base64') === file - if (buffer) return file - - throw new Error('未知类型') - } - - /** - * 将数据流对象转换为Buffer对象 - * @param {stream.Readable} stream - 要转换的数据流对象 - * @returns {Promise} - 返回Buffer - */ - stream (stream) { - return new Promise((resolve, reject) => { - const chunks = [] - stream.on('data', (chunk) => chunks.push(chunk)) - stream.on('end', () => resolve(Buffer.concat(chunks))) - stream.on('error', (error) => reject(error)) - }) - } - - /** - * 将文件转换为Buffer对象 - * @param {string|Buffer|http|stream.Readable} file - 文件路径或Buffer对象、可读流对象、http地址、base64://字符串 - * @param {object} options - 附加数据 - * @param {boolean} options.http - 为true时,http地址会直接返回,否则会下载文件并转换为Buffer对象 - * @returns {Promise} - 返回Buffer对象 - */ - async buffer (file, options = { http: false }) { - if (typeof file !== 'string') { - if (Buffer.isBuffer(file)) return file - if (file instanceof Readable) return this.stream(file) - throw new Error('未知文件类型:', file) - } - - if (file.startsWith('base64://')) return Buffer.from(file.replace('base64://', ''), 'base64') - - if (file.startsWith('http://') || file.startsWith('https://')) { - if (options.http) return file - const response = await axios.get(file, { responseType: 'arraybuffer' }) - return Buffer.from(response.data) - } - - /** file:/// */ - if (fs.existsSync(file.replace(/^file:\/\/\//, ''))) { - file = file.replace(/^file:\/\/\//, '') - return fs.readFileSync(file) - } - - /** file:// */ - if (fs.existsSync(file.replace(/^file:\/\//, ''))) { - file = file.replace(/^file:\/\//, '') - return fs.readFileSync(file) - } - - /** 无前缀base64:// */ - const buffer = Buffer.from(file, 'base64') - if (buffer.toString('base64') === file) return buffer - - throw new Error('未知类型') - } - - /** - * 标准化发送的消息内容 - * @param {string|import('../bot/KarinElement.js').KarinElement|import('../bot/KarinElement.js').KarinElement[]} elements - 消息内容 - * @returns {import('../bot/KarinElement.js').KarinElement[]} - 返回标准化处理后的消息内容 - */ - makeMessage (elements) { - /** 将msg格式化为数组 */ - if (!Array.isArray(elements)) elements = [elements] - elements = elements.map(element => { - /** 对字符串进行标准化处理 */ - if (typeof element === 'string') element = this.#segment.text(element) - return element - }) - return elements - } - - /** - * 制作简单转发,返回segment.node[]。仅简单包装node,也可以自己组装 - * @param {Array<{object}> | object} elements - * @param fakeUin 用户id - * @param fakeNick 用户昵称 - * @return {Array} - */ - makeForward (elements, fakeUin = '', fakeNick = '') { - if (!Array.isArray(elements)) { - elements = [elements] - } - return elements.map((element) => { - element = this.makeMessage(element) - return this.#segment.node(fakeUin, fakeNick, element) - }) - } - - /** - * 获取所有插件列表 - * @param {boolean} [isDir] - 返回绝对路径 - * @param {boolean} [isPack] - 屏蔽不带package.json的插件 - * @returns {Array} - 返回插件列表 - */ - getPlugins (isDir = false, isPack = false) { - const dir = this.absPath('./plugins', isDir) - let list = fs.readdirSync(dir, { withFileTypes: true }) - // 忽略非文件夹、非 karin-plugin-、karin-adapter- 开头的文件夹 - list = list.filter(v => v.isDirectory() && (v.name.startsWith('karin-plugin-') || v.name.startsWith('karin-adapter-'))) - if (isPack) list = list.filter(v => fs.existsSync(`${dir}/${v.name}/package.json`)) - - return list - } - - /** - * 获取运行时间 - * @returns {string} - 返回运行时间 - */ - uptime () { - const time = process.uptime() - const day = Math.floor(time / 86400) - const hour = Math.floor((time % 86400) / 3600) - const min = Math.floor((time % 3600) / 60) - const sec = Math.floor(time % 60) - - const parts = [ - day ? `${day}天` : '', - hour ? `${hour}小时` : '', - min ? `${min}分钟` : '', - (!day && sec) ? `${sec}秒` : '', - ] - - return parts.filter(Boolean).join('') - } - - /** - * 构建消息体日志 - * @param {import('../bot/KarinElement.js').KarinElement[]} message - 消息体 - * @returns {string} - 返回标准化处理后的日志 - */ - makeMessageLog (message) { - const logs = [] - for (const val of message) { - switch (val.type) { - case 'text': - logs.push(val.text) - break - case 'face': - logs.push(`[face:${val.id}]`) - break - case 'video': - case 'image': - case 'voice': - case 'record': - case 'file': { - let file - if (Buffer.isBuffer(val.file)) { - file = 'Buffer://...' - } else if (/^http|^file/.test(val.file)) { - file = val.file - } else { - file = 'base64://...' - } - logs.push(`[${val.type}:${file}]`) - break - } - case 'at': - logs.push(`[at:${val.uid}]`) - break - case 'rps': - logs.push(`[rps:${val.id}]`) - break - case 'dice': - logs.push(`[dice:${val.id}]`) - break - case 'shake': - logs.push('[shake:窗口抖动]') - break - case 'poke': - logs.push(`[poke:${val.id}]`) - break - case 'anonymous': - logs.push(`[anonymous:${val.ignore}]`) - break - case 'share': - logs.push(`[share:${JSON.stringify(val)}]`) - break - case 'contact': - logs.push(`[contact:${JSON.stringify(val)}]`) - break - case 'location': - logs.push(`[location:${JSON.stringify(val)}]`) - break - case 'music': - logs.push(`[music:${JSON.stringify(val)}]`) - break - case 'reply': - logs.push(`[reply:${val.message_id}]`) - break - case 'forward': - logs.push(`[forward:${val.id}]`) - break - case 'node': - logs.push(`[node:${JSON.stringify(val)}]`) - break - case 'xml': - logs.push(`[xml:${val.data}]`) - break - case 'json': - logs.push(`[json:${val.data}]`) - break - case 'markdown': { - if (val.content) { - logs.push(`[markdown:${val.content}]`) - } else { - const content = { id: val.custom_template_id } - for (const v of val.params) content[v.key] = v.values[0] - logs.push(`[markdown:${JSON.stringify(content)}]`) - } - break - } - case 'rows': { - const rows = [] - for (const v of val.rows) rows.push(v.log) - logs.push(`[rows:${JSON.stringify(rows)}]`) - break - } - case 'button': - logs.push(`[button:${val.log}]`) - break - case 'long_msg': - logs.push(`[long_msg:${val.id}]`) - break - default: - logs.push(`[未知:${JSON.stringify(val)}]`) - } - } - return logs.join('') - } -} - -/** - * @typedef {object} getPlugins 插件列表 - * @property {string} getPlugins.dir 插件目录 - * @property {string} getPlugins.name 插件名称 - */ diff --git a/lib/core/config.js b/lib/core/config.js deleted file mode 100644 index edd6556..0000000 --- a/lib/core/config.js +++ /dev/null @@ -1,355 +0,0 @@ -import fs from 'fs' -import path from 'path' -import Yaml from 'yaml' -import chokidar from 'chokidar' - -/** - * 配置文件 - */ -export default class Config { - constructor () { - /** 缓存 */ - this.change = {} - /** 监听文件 */ - this.watcher = {} - /** 拦截器状态 */ - this.review = false - this.initCfg() - this.group() - } - - /** - * 初始化配置 - */ - initCfg () { - /** - * 初始化文件夹 - */ - const list = [ - './data', - './logs', - './resources', - './temp', - './temp/html', - './config/config', - ] - - list.forEach(path => this.mkdir(path)) - const dir = process.cwd() - const CfgDir = dir + '/config/config' - const DefDir = dir + '/config/defSet' - - /** - * 初始化配置文件 - */ - const files = fs.readdirSync(DefDir).filter(file => file.endsWith('.yaml')) - files.forEach(file => { - const path = `${CfgDir}/${file}` - if (!fs.existsSync(path)) fs.copyFileSync(`${DefDir}/${file}`, path) - }) - - this.pluginDir() - } - - /** - * 创建插件文件夹文件夹 - */ - async pluginDir () { - const pluginDir = [] - const plugins = this.getPlugins() - - plugins.forEach(plugin => { - pluginDir.push(`./data/${plugin}`) - pluginDir.push(`./temp/${plugin}`) - pluginDir.push(`./resources/${plugin}`) - pluginDir.push(`./temp/html/${plugin}`) - }) - - pluginDir.forEach(path => this.mkdir(path)) - } - - /** - * 递归创建目录 - * @param {string} dirname - 要创建的文件夹路径 - */ - mkdir (dirname) { - if (fs.existsSync(dirname)) return true - /** 递归自调用 */ - if (this.mkdir(path.dirname(dirname))) fs.mkdirSync(dirname) - return true - } - - /** - * 获取插件列表 - * @returns {string[]} - 插件名称 - */ - getPlugins () { - const files = fs.readdirSync('./plugins', { withFileTypes: true }) - /** 过滤掉非 karin-plugin- 、karin-adapter- 开头的文件夹 */ - return files.filter(file => file.isDirectory() && (file.name.startsWith('karin-plugin-') || file.name.startsWith('karin-adapter-'))).map(dir => dir.name) - } - - /** - * 超时时间 - * @returns {number} - */ - timeout (type = 'ws') { - let timeout = 60 - if (type === 'ws') { - timeout = this.Server.websocket.timeout - } else { - timeout = this.Server.grpc.timeout - } - return timeout || 60 - } - - /** - * Redis 配置 - * 采用实时读取优化性能 - * @returns {redis} - * @typedef {Object} redis - * @property {string} host - 地址 - * @property {number} port - 端口 - * @property {string} username - 用户名 - * @property {string} password - 密码 - * @property {number} db - 数据库 - * @property {object} cluster - 集群配置 - * @property {string} cluster.enable - 是否开启集群 - * @property {{url: string}[]} cluster.rootNodes - 集群节点 - */ - get redis () { - const config = this.getYaml('config', 'redis', false, false) - const defSet = this.getYaml('defSet', 'redis', false, false) - const data = { ...defSet, ...config } - return data - } - - /** - * 主人列表 - * @returns {string[]} - */ - get master () { - return this.Config.master || [] - } - - /** - * 管理员列表 - * @returns {string[]} - */ - get admin () { - return this.Config.admin || [] - } - - /** App管理 */ - get App () { - const key = 'change.App' - /** 取缓存 */ - if (this.change[key]) { - return this.change[key] - } - - /** 取配置 */ - const config = this.getYaml('config', 'App', true) - const defSet = this.getYaml('defSet', 'App', true) - const data = { ...defSet, ...config } - /** 缓存 */ - this.change[key] = data - return this.change[key] - } - - /** - * 功能黑名单 - * @returns {string[]} - */ - get blackList () { - return this.Config.BlackList || [] - } - - /** - * @returns {Cfg} - 基本配置 - * @typedef {Object} Cfg - * @property {string} log_level - 日志等级 - * @property {string} log_color - 触发插件函数的颜色 - * @property {number} log_days_Keep - 日志保存天数 - * @property {boolean} multi_progress - 关闭后台进程失败后是否继续启动 - * @property {string} ffmpeg_path - ffmpeg路径 - * @property {string} ffprobe_path - ffprobe路径 - * @property {string[]} master - 主人列表 - * @property {string[]} admin - 管理员列表 - * @property {Object} BlackList - 黑名单相关 - * @property {string[]} BlackList.users - 黑名单用户 - * @property {string[]} BlackList.groups - 黑名单群聊 - * @property {string[]} BlackList.GroupMsgLog - 消息日志黑名单群聊 - * @property {Object} WhiteList - 白名单相关 - * @property {string[]} WhiteList.users - 白名单用户 - * @property {string[]} WhiteList.groups - 白名单群聊 - * @property {string[]} WhiteList.GroupMsgLog - 消息日志白名单群聊 - */ - get Config () { - const key = 'change.config' - /** 取缓存 */ - if (this.change[key]) { - return this.change[key] - } - - /** 取配置 */ - const config = this.getYaml('config', 'config', true) - const defSet = this.getYaml('defSet', 'config', false) - const data = { ...defSet, ...config } - /** 缓存 */ - this.change[key] = data - return this.change[key] - } - - /** - * Server 配置文档 - * @returns {Server} - 服务配置 - * @typedef {Object} Server - 服务配置 - * @property {boolean} Server.HotUpdate - 当前文件热更新是否重启http、grpc服务 - * - * @property {Object} Server.http - http 服务器配置 - * @property {string} Server.http.host - 监听地址 - * @property {number} Server.http.port - 端口 - * - * @property {Object} Server.grpc - grpc 服务器配置 - * @property {string} Server.grpc.host - 监听地址 - * @property {number} Server.grpc.timeout - Api请求超时时间(秒) - * - * @property {Object} Server.websocket - websocket 服务器配置 - * @property {number} Server.websocket.timeout - API请求超时时间(秒) - * @property {string[]} Server.websocket.render - websocket 渲染器地址 - * @property {string[]} Server.websocket.OneBot11Host - onebot11 正向WebSocket地址 - * @property {string[]} Server.websocket.OneBot12Host - onebot12 正向WebSocket地址 - * - * @property {Object} Server.HttpRender - HTTP渲染器配置 - * @property {boolean} Server.HttpRender.enable - 是否开启http渲染 - * @property {string} Server.HttpRender.host - karin端Api地址 公网 > 局域网 > 127 - * @property {string} Server.HttpRender.post - karin-puppeteer渲染器 post请求地址 - * @property {string} Server.HttpRender.token - karin-puppeteer渲染器 post请求token - * @property {string} Server.HttpRender.not_found - 请求的非html或非有效路径的返回内容 可以填http地址 例如:https://ys.mihoyo.com - * @property {string} Server.HttpRender.WormholeClient - Wormhole代理地址 无公网环境的情况下使用 - */ - get Server () { - const key = 'change.server' - /** 取缓存 */ - if (this.change[key]) { - return this.change[key] - } - - /** 取配置 */ - const config = this.getYaml('config', 'server', true) - const defSet = this.getYaml('defSet', 'server', false) - const data = { ...defSet, ...config } - /** 缓存 */ - this.change[key] = data - return this.change[key] - } - - /** - * package.json - * 实时获取package.json文件 - */ - get package () { - let data = fs.readFileSync('package.json', 'utf8') - data = JSON.parse(data) - return data - } - - /** - * 获取群配置 - * @param {string} group_id 群号 - * @returns {Group} - 群配置 - * @typedef {Object} Group - 群配置 - * @property {number} Group.GroupCD - 群聊中所有消息冷却时间,单位秒,0则无限制 - * @property {number} Group.GroupUserCD - 群聊中 每个人的消息冷却时间,单位秒,0则无限制 - * @property {0|1|2|3|4|5} Group.mode - 机器人响应模式,0-所有 1-仅@机器人 2-仅回应主人 3-仅回应前缀 4-前缀或@机器人 5-主人无前缀,群员前缀或@机器人 - * @property {string[]} Group.alias - 机器人前缀 - * @property {string[]} Group.enable - 白名单插件、功能,只有在白名单中的插件、功能才会响应 - * @property {string[]} Group.disable - 黑名单插件、功能,黑名单中的插件、功能不会响应 - */ - group (group_id = '') { - const key = 'change.group' - /** 取缓存 */ - if (this.change[key]) { - const res = { ...this.change[key].defCfg.default, ...this.change[key].Config.default, ...(this.change[key].Config[group_id] || {}) } - return res - } - - /** 取配置 */ - const Config = this.getYaml('config', 'group', true) - const defCfg = this.getYaml('defSet', 'group') - const data = { Config, defCfg } - /** 缓存 */ - this.change[key] = data - const res = { ...defCfg.default, ...Config.default, ...(Config[group_id] || {}) } - return res - } - - /** - * 获取配置yaml - * @param {'defSet'|'config'} type 类型 - * @param {string} name 文件名称 不带后缀 - * @param {boolean} isWatch 是否监听文件变化 - */ - getYaml (type, name, isWatch) { - /** 文件路径 */ - const file = `./config/${type}/${name}.yaml` - - /** 读取文件 */ - const data = Yaml.parse(fs.readFileSync(file, 'utf8')) - - /** 监听文件 */ - if (isWatch) this.watch(type, name, file) - return data - } - - /** - * 监听配置文件 - * @param {'defSet'|'config'} type 类型 - * @param {string} name 文件名称 不带后缀 - * @param {string} file 文件路径 - */ - async watch (type, name, file) { - const key = `change.${name}` - /** 已经监听过了 */ - if (this.watcher[key]) return true - /** 监听文件 */ - const watcher = chokidar.watch(file) - /** 监听文件变化 */ - watcher.on('change', () => { - delete this.change[key] - global.logger.mark(`[修改配置文件][${type}][${name}]`) - /** 文件修改后调用对应的方法 */ - if (this[`change_${name}`]) this[`change_${name}`]() - }) - /** 缓存 防止重复监听 */ - this.watcher[key] = watcher - } - - async change_App () { - await this.#review() - } - - async change_config () { - /** 修改日志等级 */ - global.logger.level = this.Config.log_level - await this.#review() - if (this.Server.HotUpdate) { - const { Bot } = await import('../index.js') - Bot.emit('restart.http', {}) - Bot.emit('restart.grpc', {}) - } - } - - async change_group () { - await this.#review() - } - - async #review () { - if (this.review) return - this.review = true - const review = await import('../event/review.js') - review.default.main() - this.review = false - } -} diff --git a/lib/core/listener.js b/lib/core/listener.js deleted file mode 100644 index 1fcb6e8..0000000 --- a/lib/core/listener.js +++ /dev/null @@ -1,247 +0,0 @@ -import { EventEmitter } from 'events' -import loader from '../plugins/loader.js' - -/** - * 监听器管理 - */ -export default class Listeners extends EventEmitter { - /** - * 日志模块 - * @private - * @type {import('../index.js').logger} - */ - #logger - - /** - * 常用方法 - * @private - * @type {import('../index.js').common} - */ - #common - - /** - * Bot索引 - * @type {number} - Bot索引 - */ - index - - /** - * 框架名称 - * @type {string} - */ - name - - /** - * 注册的Bot列表 - * @type {{index: number, type: adapterType, bot: import('../adapter/adapter.js').KarinAdapter}[]} - */ - list - - /** - * 注册的适配器列表 - * @type {{type: adapterType, adapter: import('../adapter/adapter.js').KarinAdapter}[]} - */ - adapter - - /** - * @param {import('../index.js').segment} segment - */ - #segment - - /** - * @param {import('../index.js').logger} logger - * @param {import('../index.js').common} common - * @param {import('../index.js').config} config - * @param {import('../index.js').segment} segment - */ - constructor (logger, common, config, segment) { - super() - this.index = 0 - this.name = 'Karin' - this.#logger = logger - this.#common = common - this.#segment = segment - this.list = [] - this.adapter = [] - this.on('error', data => logger.error(data)) - this.on('plugin', () => loader.load()) - this.on('adapter', data => { - let path = data.path || '无' - if (path && data.type !== 'grpc') path = `ws://127.0.0.1:/${config.Server.http.port}${data.path}` - path = logger.green(path) - logger.info(`[适配器][注册][${data.type}] ` + path) - this.addAdapter(data) - }) - this.on('bot', data => { - this.addBot(data) - logger.info(`[机器人][注册][${data.type}] ` + logger.green(`[account:${data.bot.account.uid || data.bot.account.uin}(${data.bot.account.name})]`)) - this.emit('karin:online', data.bot.account.uid || data.bot.account.uin) - }) - } - - /** - * 注册Bot 返回索引id - * @typedef {object} addBot - * @property {import('../adapter/adapter.js').KarinAdapter|false} bot - Bot实例 - * @property {adapterType} type - Bot适配器类型 - * @param {addBot} data - Bot数据 - * @returns {number|false} - 注册成功返回Bot的索引id - */ - addBot (data) { - this.index++ - const index = this.index - if (!data.bot) { - this.#logger.error('[Bot管理][注册] 注册失败: Bot实例不能为空') - return false - } - - this.list.push({ index, type: data.type, bot: data.bot }) - return index - } - - /** - * 卸载Bot - * @param {number} index - Bot的索引id - */ - delBot (index) { - this.list = this.list.filter(item => item.index !== index) - } - - /** - * 通过Bot uid 获取Bot - * @param {string} [uid] - Bot的uid 未传入则返回第一个Bot - * @returns {import('../adapter/adapter.js').KarinAdapter} - */ - getBot (uid = '') { - uid = String(uid) - if (this.list.length === 0) { - this.#logger.error('[Bot管理][UID] 当前Bot列表为空') - return undefined - } - - if (!uid) return this.list[0].bot - - const index = this.list.findIndex(item => String(item.bot.account.uid) === uid) - if (index === -1) { - this.#logger.error('[Bot管理][UID] 无法找到对应的 Bot 实例') - return undefined - } - - return this.list[index].bot - } - - /** - * 根据索引获取Bot - * @param {number} index - Bot的索引id - * @returns {import('../adapter/adapter.js').KarinAdapter} - */ - getBotByIndex (index) { - index = this.list.findIndex(item => item.index === index) - if (index === -1) { - throw new Error('[Bot管理][索引] 无法找到对应的 Bot 实例') - } - return this.list[index].bot - } - - /** - * 获取当前已注册Bot数量 - */ - getBotCount () { - return this.list.length - } - - /** - * 获取所有Bot列表 - * @param {boolean} [isIndex] - 是否返回包含的索引列表 默认返回Bot实例列表 - */ - getBotAll (isIndex = false) { - if (isIndex) return this.list - return this.list.map(item => item.bot) - } - - /** - * 注册适配器 - * @param {addAdapter} data - 适配器数据 - * @typedef {object} addAdapter - * @property {adapterType} addAdapter.type - 适配器类型 - * @property {import('../adapter/adapter.js').KarinAdapter} addAdapter.adapter - 适配器实例 - * @property {string} [addAdapter.path] - 适配器路径 仅适用于反向WS适配器 - */ - addAdapter (data) { - const adapter = { type: data.type, adapter: data.adapter } - if (data.path) adapter.path = data.path - this.adapter.push(adapter) - } - - /** - * 通过path获取适配器 仅适用于反向WS适配器 - * @param {string} path - 适配器路径 - */ - getAdapter (path) { - const index = this.adapter.findIndex(item => item?.path === path) - if (index === -1) { - this.#logger.error('[适配器管理] 无法找到对应的适配器实例') - return false - } - return this.adapter[index].adapter - } - - /** - * 获取适配器列表 - * @param {boolean} isType - 是否返回包含的类型列表 默认返回适配器实例列表 - * @returns {import('../adapter/adapter.js').KarinAdapter[]|{type: adapterType, path: string, adapter: import('../adapter/adapter.js').KarinAdapter}[]} - */ - getAdapterAll (isType = false) { - if (isType) return this.adapter - return this.adapter.map(item => item.adapter) - } - - /** - * 发送主动消息 - * @param {string} uid - Bot的uid - * @param {import('../bot/KarinElement.js').KarinContact} contact - 目标信息 - * @param {Array|KarinElement|string} elements - 消息内容 - * @param {object} options - 消息选项 - * @param {number} options.recallMsg - 发送成功后撤回消息时间 - * @param {number} options.retry_count - 重试次数 - * @returns {Promise<{message_id}>} - */ - async sendMsg (uid, contact, elements, options = { recallMsg: 0, retry_count: 1 }) { - const bot = this.getBot(uid) - if (!bot) throw new Error('发送消息失败: 未找到对应Bot实例') - const { recallMsg, retry_count } = options - /** 标准化 */ - elements = this.#common.makeMessage(elements) - - /** 先发 提升速度 */ - let result = bot.SendMessage(contact, elements, retry_count) - - const reply_log = this.#common.makeMessageLog(elements) - const self_id = bot.account.uid || bot.account.uin - if (contact.scene === 'group') { - this.#logger.bot('info', self_id, `Send Proactive Group ${contact.peer}: ${reply_log}`) - } else { - this.#logger.bot('info', self_id, `Send Proactive private ${contact.peer}: ${reply_log}`) - } - - try { - this.emit('karin:count:send', 1) - /** 取结果 */ - result = await result - this.#logger.bot('debug', self_id, `主动消息结果:${JSON.stringify(result, null, 2)}`) - } catch (err) { - this.#logger.bot('error', self_id, `主动消息发送失败:${reply_log}`) - this.#logger.bot('error', self_id, err) - } - - /** 快速撤回 */ - if (bot.RecallMessage && recallMsg > 0 && result?.message_id) { - setTimeout(() => bot.RecallMessage(null, result.message_id), recallMsg * 1000) - } - return result - } -} - -/** - * @typedef {'internal'|'websocket'|'grpc'|'http'|'render'} adapterType - */ diff --git a/lib/core/logger.js b/lib/core/logger.js deleted file mode 100644 index 4c0fc49..0000000 --- a/lib/core/logger.js +++ /dev/null @@ -1,125 +0,0 @@ -import chalk from 'chalk' -import log4js from 'log4js' - -export default class Logger { - constructor () { - this.log_color = '#FFFF00' - this.options = {} - process.env.KarinMode = process.argv[2]?.includes('dev') ? 'dev' : 'prod' - } - - /** - * 默认配置 - * @param {Object} Config - * @returns {Logger} - */ - config (Config) { - this.log_color = Config.Config.log_color - this.options = { - appenders: { - console: { - type: 'console', - layout: { - type: 'pattern', - pattern: `%[[Karin][%d{hh:mm:ss.SSS}][%4.4p]%] ${process.env.KarinMode === 'dev' ? '[%f{3}:%l] ' : ''}%m`, - }, - }, - out: { - /** 输出到文件 */ - type: 'file', - filename: 'logs/logger', - pattern: 'yyyy-MM-dd.log', - /** 日期后缀 */ - keepFileExt: true, - /** 日志文件名中包含日期模式 */ - alwaysIncludePattern: true, - /** 日志文件保留天数 */ - daysToKeep: Config.Config.log_days_Keep, - /** 日志输出格式 */ - layout: { - type: 'pattern', - pattern: '[%d{hh:mm:ss.SSS}][%4.4p] %m', - }, - }, - }, - categories: { - default: { - appenders: ['out', 'console'], - level: Config.Config.log_level, - enableCallStack: process.env.KarinMode === 'dev', - }, - }, - } - return this - } - - /** - * CLI 配置 - * @returns {Logger} - */ - cli () { - this.options = { - appenders: { - console: { - type: 'console', - layout: { - type: 'pattern', - pattern: '%[[Karin-cli][%d{hh:mm:ss.SSS}][%4.4p]%] %m', - }, - }, - }, - categories: { - default: { - appenders: ['console'], - level: 'info', - }, - }, - } - return this - } - - /** - * 日志模块 - * @returns {LoggerType} - */ - logger () { - log4js.configure(this.options) - const logger = log4js.getLogger('default') - logger.chalk = chalk - logger.red = chalk.red - logger.green = chalk.green - logger.yellow = chalk.yellow - logger.blue = chalk.blue - logger.magenta = chalk.magenta - logger.cyan = chalk.cyan - logger.white = chalk.white - logger.gray = chalk.gray - logger.violet = chalk.hex('#868ECC') - logger.fnc = chalk.hex(this.log_color || '#FFFF00') - logger.bot = (level, id, ...args) => logger[level](logger.violet(`[Bot:${id}] `) + args.join(' ')) - - return Object.freeze(logger) - } -} - -/** - * @type {LoggerType} - */ -export const LoggerType = {} - -/** - * @typedef {log4js.Logger & { - * chalk: import('chalk').default, - * red: (text: string) => string, - * green: (text: string) => string, - * yellow: (text: string) => string, - * blue: (text: string) => string, - * magenta: (text: string) => string, - * cyan: (text: string) => string, - * white: (text: string) => string, - * gray: (text: string) => string, - * violet: (text: string) => string, - * fnc: (text: string) => string, - * bot: (level: 'trace'|'debug'|'mark'|'info'|'warn'|'error'|'fatal', id: string, ...args: string[]) => void - * }} LoggerType - */ diff --git a/lib/core/online.js b/lib/core/online.js deleted file mode 100644 index 5885a5b..0000000 --- a/lib/core/online.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * 发送上线通知 - */ -export default class Online { - constructor (level, segment, listener) { - this.level = level - this.segment = segment - this.listener = listener - this.listener.on('karin:online', this.start.bind(this)) - setTimeout(() => this.listener.off('karin:online', this.start.bind(this)), 30000) - } - - async start (uid) { - /** 重启 */ - const key = `karin:restart:${uid}` - let options = '' - try { - options = await this.level.get(key) - } catch { } - if (!options) return - const { id, contact, time, message_id } = options - /** 重启花费时间 保留2位小数 */ - const restartTime = ((Date.now() - time) / 1000).toFixed(2) - /** 如果超过5分钟 则删除并跳过 */ - if (restartTime > 300) { - await this.level.del(key) - return false - } - - const element = [ - this.segment.reply(message_id), - this.segment.text(`Karin 重启成功:${restartTime}秒`), - ] - await this.listener.sendMsg(id, contact, element) - await this.level.del(key) - return true - } -} diff --git a/lib/core/process.js b/lib/core/process.js deleted file mode 100644 index 923a565..0000000 --- a/lib/core/process.js +++ /dev/null @@ -1,160 +0,0 @@ -import axios from 'axios' - -/** - * 处理基本事件 - */ -export default class Process { - /** - * @type {import('../index.js').logger} - */ - logger - /** - * @type {import('../index.js').common} - */ - common - /** - * @type {import('../index.js').Config} - */ - Config - - /** - * @type {import('../index.js').listener} - */ - listener - - /** - * @param {import('../index.js').logger} logger - * @param {import('../index.js').common} common - * @param {import('../index.js').Config} Config - * @param {import('../index.js').listener} listener - */ - constructor (logger, common, Config, listener) { - this.logger = logger - this.common = common - this.Config = Config - this.listener = listener - } - - /** - * 进程初始化 - * @returns {Process} - */ - process () { - /** - * 启动日志 - */ - this.logger.mark('Karin 启动中...') - /** - * 设置标题 - */ - process.title = 'Karin' - /** - * 设置时区 - */ - process.env.TZ = 'Asia/Shanghai' - /** - * 监听挂起信号 在终端关闭时触发 - */ - process.once('SIGHUP', async (code) => await this.exit(code)) - /** - * 监听中断信号 用户按下 Ctrl + C 时触发 - */ - process.once('SIGINT', async (code) => await this.exit(code)) - /** - * 监听终止信号 程序正常退出时触发 - */ - process.once('SIGTERM', async (code) => await this.exit(code)) - /** - * 监听退出信号 windows下程序正常退出时触发 - */ - process.once('SIGBREAK', async (code) => await this.exit(code)) - /** - * 监听退出信号 与 SIGINT 类似,但会生成核心转储 - */ - process.once('SIGQUIT', async (code) => await this.exit(code)) - /** - * 监听退出信号 Node.js进程退出时触发 - */ - process.once('exit', async (code) => await this.exit(code)) - /** - * 捕获警告 - */ - process.on('warning', warning => this.listener.emit('warn', warning)) - /** - * 捕获错误 - */ - process.on('uncaughtException', error => this.listener.emit('error', error)) - /** - * 捕获未处理的Promise错误 - */ - process.on('unhandledRejection', error => this.listener.emit('error', error)) - return this - } - - /** - * 检查后台进程 - * @returns {Promise} 是否检测到后台进程 - */ - async check () { - const host = `http://localhost:${this.Config.Server.http.port}/api` - /** - * 使用api来检查后台 - */ - const res = await this.axios(host + '/info', 100) - if (res) return this - - this.logger.mark(this.logger.red('检测到后台进程 正在关闭')) - /** 发退出信号 */ - await this.axios(host + '/exit', 10) - - for (let i = 0; i < 50; i++) { - const res = await this.axios(host + '/info', 100) - /** 请求成功继续循环 */ - if (!res) continue - /** 请求异常即代表后台进程已关闭 */ - this.logger.mark(this.logger.green('后台进程已关闭')) - return this - } - - /** - * 走到这里说明后台关闭失败 - * 根据配置文件判断是否继续 - */ - this.logger.error(this.logger.red('后台进程关闭失败,请手动关闭')) - if (!this.Config.Config.multi_progress) { - this.logger.error(this.logger.red('当前配置不允许多进程运行,程序即将退出')) - await this.exit(1) - } - this.logger.error(this.logger.red('当前配置允许多进程运行,程序继续运行')) - return this - } - - /** - * 请求 - * @param {string} url - * @param {number} timeout 超时时间 - * @returns {Promise} 是否请求失败 - */ - async axios (url, timeout) { - try { - await axios.get(url, { timeout }) - return false - } catch { - return true - } - } - - /** - * 退出Karin - * @param {number} code 退出码 - */ - async exit (code) { - try { - const { redis } = await import('#Karin') - if (redis && redis.save) await redis.save() - this.logger.mark(`Karin 已停止运行 运行时间:${this.common.uptime()} 退出码:${code || '未知'}`) - } finally { - process.exit() - } - } -} diff --git a/lib/core/server.js b/lib/core/server.js deleted file mode 100644 index 6e94dac..0000000 --- a/lib/core/server.js +++ /dev/null @@ -1,261 +0,0 @@ -import fs from 'fs' -import express from 'express' -import { createServer } from 'http' -import { WebSocketServer } from 'ws' -import Renderer from '../renderer/App.js' -import connect from '../renderer/Wormhole.js' -import HttpRenderer from '../renderer/Http.js' - -export default class Server { - /** - * @private - * @type {import('../index.js').logger} - */ - #logger - - /** - * @private - * @type {import('../index.js').common} - */ - #common - - /** - * @private - * @type {import('../index.js').config} - */ - #config - - /** - * @private - * @type {import('../index.js').listener} - */ - #listener - - /** - * @private - * @type {import('../index.js').exec} - */ - #exec - - /** - * @param {import('../index.js').logger} logger - * @param {import('../index.js').common} common - * @param {import('../index.js').config} config - * @param {import('../index.js').listener} listener - * @param {import('../index.js').exec} exec - */ - constructor (logger, common, config, listener, exec) { - this.#exec = exec - this.#logger = logger - this.#common = common - this.#config = config - this.#listener = listener - - this.reg = '' - this.list = [] - this.app = express() - this.server = createServer(this.app) - this.WebSocketServer = new WebSocketServer({ server: this.server }) - this.RegExp = new RegExp(`(${process.cwd()}|${process.cwd().replace(/\\/g, '/')})`, 'g') - } - - /** - * 监听WebSocket连接并初始化http服务器 - * @returns {Server} - */ - init () { - try { - this.WebSocketServer.on('connection', (socket, request) => { - const path = request.url - const headers = request.headers - this.#logger.debug('[反向WS]', path, JSON.stringify(headers, null, 2)) - try { - const Adapter = this.#listener.getAdapter(path) - if (!Adapter) { - this.#logger.error(`[反向WS] 适配器不存在:${path}`) - return socket.close() - } - new Adapter(this.#logger, this.#common, this.#listener, this.#config).server(socket, request) - } catch (error) { - this.#logger.error(`[反向WS] 注册适配器发生错误:${path}`, error) - socket.close() - } - }) - - /** 监听连接断开 */ - this.WebSocketServer.on('close', (socket, request) => this.#logger.error('WebSocket断开', request.url)) - - /** GET接口 - 获取当前启动信息 */ - this.app.get('/api/info', (req, res) => { - /** 只允许本机ip访问 */ - if (req.hostname === 'localhost' || req.hostname === '127.0.0.1') { - const data = { - start: process.env.pm_id ? 'pm2' : 'node', - start_time: process.uptime().toFixed(2), - memory: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2), - time: Date.now(), - } - - res.json(data) - } - }) - - /** GET接口 - 退出当前进程 */ - this.app.get('/api/exit', async (req) => { - /** 只允许本机ip访问 */ - if (req.hostname === 'localhost' || req.hostname === '127.0.0.1') { - this.#logger.mark('[服务器][HTTP] 收到退出请求,即将退出') - /** 关闭服务器 */ - this.#listener.emit('exit.grpc') - this.server.close() - /** 如果是pm2 获取当前pm2ID 使用 */ - if (process.env.pm_id) await this.#exec(`pm2 delete ${process.env.pm_id}`) - /** 正常启动的进程 */ - process.exit() - } - }) - - /** 监听端口 */ - const { host, port } = this.#config.Server.http - this.server.listen(port, host, () => { - this.#logger.mark('[服务器][启动成功][HTTP]: ' + this.#logger.green(`http://${host}:${port}`)) - }) - - this.#listener.once('restart.http', () => { - this.#logger.mark('[服务器][重启][HTTP] 正在重启HTTP服务器...') - this.#restartServer() - }) - - const { enable, WormholeClient } = this.#config.Server.HttpRender - if (enable) { - this.static() - if (WormholeClient) { - connect(this.#config) - return this - } - - const { host, post, token } = this.#config.Server.HttpRender - /** 注册渲染器 */ - const rd = new HttpRenderer(host, post, token) - Renderer.app({ id: 'puppeteer', type: 'image', render: rd.render.bind(rd) }) - } - - return this - } catch (error) { - this.#logger.error('初始化HTTP服务器失败: ', error) - return false - } - } - - /** - * HTTP渲染器 - */ - static () { - this.staticPath() - - /** GET接口 - 渲染 */ - this.app.get('/api/renderHtml', (req, res) => { - try { - let { html } = req.query - html = decodeURIComponent(html).replace(/\\/g, '/').replace(/^\.\//, '') - /** 判断是否为html文件且路径存在 */ - if (!html.endsWith('.html') || !fs.existsSync(html)) { - const not_found = this.#config.Server.HttpRender.not_found - if (not_found.startsWith('http')) { - return res.redirect(not_found) - } else { - return res.status(404).send(JSON.stringify({ code: 404, msg: not_found || '?' })) - } - } - - let content = fs.readFileSync(html, 'utf-8') - /** 处理所有绝对路径、相对路径 */ - content = content.replace(this.RegExp, '') - res.send(content) - } catch (e) { - this.#logger.error('[服务器][GET接口 - 渲染]', e) - res.status(500).send(JSON.stringify({ code: 500, msg: 'Internal Server Error' })) - } - }) - - /** 拦截静态资源 防止恶意访问 */ - this.app.use((req, res, next) => { - this.#logger.debug(`[静态资源][${req.headers.host}] ${req.url}`) - - /** 解码 */ - req.url = decodeURIComponent(req.url) - req.url = req.url.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^(\.\.\/)+/, '') - - /** 拦截非资源文件 */ - this.reg.lastIndex = 0 - if (!this.reg.test(req.url)) { - this.#logger.mark(`[${req.ip}][拦截非资源文件]`, req.url) - res.status(404).send(JSON.stringify({ code: 404, msg: 'Not Found' })) - return - } - next() - }) - - /** 设置静态文件目录 */ - this.app.use(express.static(process.cwd())) - } - - /** - * 构建静态资源路径 - */ - staticPath () { - this.list = [] - /** 读取./resources文件夹 */ - const resDir = './resources' - const resdirs = fs.readdirSync(resDir) - for (const dir of resdirs) { - const file = `${resDir}/${dir}` - if (this.#common.isDir(file)) this.list.push(file.replace('.', '')) - } - - /** 读取./temp/html下所有文件夹 */ - const htmlDir = './temp/html' - const dirs = fs.readdirSync(htmlDir) - for (const dir of dirs) { - const file = `${htmlDir}/${dir}` - if (this.#common.isDir(file)) this.list.push(file.replace('.', '')) - } - - /** 读取./plugins/html下所有文件夹 */ - const pluginsDir = './plugins' - const plugins = fs.readdirSync(pluginsDir) - for (const dir of plugins) { - const file = `${pluginsDir}/${dir}` - const resFile = `${file}/resources` - /** 包含resources文件夹 */ - if (this.#common.isDir(file) && this.#common.isDir(resFile)) this.list.push(resFile.replace('.', '')) - const componentsFile = `${file}/components` - /** 包含components文件夹 兼容mys */ - if (this.#common.isDir(file) && this.#common.isDir(componentsFile)) this.list.push(componentsFile.replace('.', '')) - } - this.reg = new RegExp(`(${this.list.join('|')})`, 'g') - } - - /** 重启当前HTTP服务器 */ - async #restartServer () { - try { - /** 断开所有 WebSocket 连接 */ - for (const ws of this.WebSocketServer.clients) { - await ws.terminate() - } - - /** 关闭当前HTTP服务器 */ - this.server.close() - /** 延迟1秒 */ - await this.#common.sleep(1000) - - /** 创建一个新的服务器实例 */ - this.server = createServer(this.app) - this.WebSocketServer = new WebSocketServer({ server: this.server }) - this.init(false) - this.static() - } catch (err) { - this.#logger.error('[服务器][重启失败]', err) - } - } -} diff --git a/lib/db/level.js b/lib/db/level.js deleted file mode 100644 index 611f446..0000000 --- a/lib/db/level.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Level } from 'level' - -const path = process.cwd() + '/data/db/Level' - -/** - * @type {Level} - */ -export default class LevelDB extends Level { - constructor () { - super(path, { valueEncoding: 'json' }) - /** - * @type {'Level'} 唯一标识符 用于区分不同的数据库 - */ - this.id = 'Level' - } - - /** - * 对get方法进行重写 找不到数据时返回null - * @param {string} key 键 - * @returns {Promise} - */ - async get (key) { - try { - return await super.get(key) - } catch { - return null - } - } - - /** - * 增加set方法 - * @param {string} key 键 - * @param {object|string} value 值 - */ - async set (key, value, ...args) { - return await super.put(key, value, ...args) - } -} diff --git a/lib/db/redis.js b/lib/db/redis.js deleted file mode 100644 index e566e24..0000000 --- a/lib/db/redis.js +++ /dev/null @@ -1,158 +0,0 @@ -import { exec } from 'child_process' -import RedisLevel from './redis_level.js' -import { createClient, createCluster } from 'redis' - -export default class Redis { - /** - * @type {import('../index.js').logger} - */ - #logger - - /** - * @private - * @type {import('../index.js').config} - */ - #config - - constructor (logger, config) { - this.RunCmd = '' - this.#logger = logger - this.#config = config - } - - /** - * redis实例 - * @return {Promise} - * @typedef {import('redis').RedisClientType} RedisClientType - */ - async start () { - const { host, port, username, password, db: database, cluster } = this.#config.redis - /** 集群模式 */ - if (cluster && cluster.enable) { - this.#logger.info('正在连接 Redis 集群...') - const { status, data } = await this.connectCluster(cluster.rootNodes) - if (status === 'ok') { - this.#logger.info('Redis 集群连接成功') - return data - } - this.#logger.error(`Redis 集群建立连接失败:${this.#logger.red(data)}`) - return false - } - - this.#logger.info(`正在连接 ${this.#logger.green(`Redis://${host}:${port}/${database}`)}`) - - const options = { socket: { host, port }, username, password, database } - - /** 第一次连接 */ - const { status, data } = await this.connect(options) - - if (status === 'ok') { - this.#logger.info('Redis 连接成功') - return data - } - - /** 第一次连接失败尝试拉起 windows直接降级 */ - if (process.platform === 'win32') { - this.#logger.error(`Redis 建立连接失败:${this.#logger.red(data)}`) - return await this.LevelDB() - } - - this.RunCmd = 'redis-server --save 900 1 --save 300 10 --daemonize yes' + await this.aarch64() - this.#logger.info('正在尝试启动 Redis...') - - try { - await this.execSync(this.RunCmd) - /** 启动成功再次重试 */ - const { status, data } = await this.connect(options) - if (status === 'ok') { - this.#logger.info('Redis 连接成功') - return data - } - - this.#logger.error(`Redis 二次建立连接失败:${this.#logger.red(data)}`) - return false - } catch (error) { - this.#logger.error(`Redis 启动失败:${this.#logger.red(error)}`) - return await this.LevelDB() - } - } - - /** - * 降级为 LevelDB - */ - async LevelDB () { - try { - this.#logger.mark(this.#logger.red('正在降级为 LevelDB 代替 Redis 只能使用基础功能')) - const redis = new RedisLevel() - this.#logger.info('LevelDB 降级成功') - return redis - } catch (error) { - this.#logger.error('降级为 LevelDB 失败') - this.#logger.error(error) - return false - } - } - - /** - * 连接 Redis 单例 - * @param {import("redis").RedisClientOptions} options - * @return {Promise<{status: 'ok', data: import("redis").RedisClientType} | {status: 'error', data: Error}>} - */ - async connect (options) { - const client = createClient(options) - try { - await client.connect() - return { status: 'ok', data: client } - } catch (error) { - return { status: 'error', data: error } - } - } - - /** - * 连接 Redis 集群 - * @param {string[]} rootNodes - * @return {Promise<{status: 'ok', data: } | {status: 'error', data: Error}>} - */ - async connectCluster (rootNodes) { - const client = createCluster({ rootNodes }) - try { - await client.connect() - return { status: 'ok', data: client } - } catch (error) { - return { status: 'error', data: error } - } - } - - /** - * 判断是否为 ARM64 并返回参数 - * @return {Promise<''|' --ignore-warnings ARM64-COW-BUG'>} - */ - async aarch64 () { - try { - /** 判断arch */ - const arch = await this.execSync('uname -m') - if (arch && arch.includes('aarch64')) { - /** 提取 Redis 版本 */ - let version = await this.execSync('redis-server -v') - if (version) { - /** 提取版本号 */ - version = version.match(/v=(\d)./) - /** 如果>=6版本则忽略警告 */ - if (version && version[1] >= 6) return ' --ignore-warnings ARM64-COW-BUG' - } - } - return '' - } catch { - return '' - } - } - - execSync (cmd) { - return new Promise((resolve, reject) => { - exec(cmd, (error, stdout) => { - if (error) return reject(error) - resolve(stdout) - }) - }) - } -} diff --git a/lib/db/redis_level.js b/lib/db/redis_level.js deleted file mode 100644 index 9a6c1f4..0000000 --- a/lib/db/redis_level.js +++ /dev/null @@ -1,309 +0,0 @@ -import { Level } from 'level' - -export default class RedisLevel { - #level - #expireMap - constructor () { - const path = process.cwd() + '/data/db/RedisLevel' - this.#level = new Level(path, { valueEncoding: 'json' }) - /** - * @type {'RedisLevel'} 唯一标识符 用于区分不同的数据库 - */ - this.id = 'RedisLevel' - /** - * 过期时间映射表 - * @type {Map} 键: 值 (过期时间) - */ - this.#expireMap = new Map() - - /** - * 开启过期时间处理 - */ - this.#expireHandle() - } - - /** - * 过期时间处理 每分钟检查一次 - */ - async #expireHandle () { - setInterval(async () => { - const now = Date.now() - // 获取代理对象的键值对数组 - const entries = Array.from(this.#expireMap.entries()) - for (const [key, expire] of entries) { - if (expire < now) { - await this.#level.del(key) - this.#expireMap.delete(key) // 通过代理的方式删除键值对 - } - } - }, 60000) - - /** - * 对expireMap进行代理 实现持久化 每次触发都保存到数据库 - */ - const handler = { - get: function (target, prop, receiver) { - if (prop === 'get' || prop === 'entries') { - return function (...args) { - const reflect = Reflect.get(target, prop).apply(target, args) - return typeof reflect == 'function' ? reflect.bind(target) : reflect - } - } - const reflect = Reflect.get(target, prop, receiver) - return typeof reflect == 'function' ? reflect.bind(target) : reflect - }, - set: function (target, key, value) { - target.set(key, value) - // 修改后持久化到数据库 - const reflect = Reflect.set(target, key, value) - return typeof reflect == 'function' ? reflect.bind(target) : reflect - }, - deleteProperty: function (target, key) { - target.delete(key) - // 删除后持久化到数据库 - const reflect = Reflect.deleteProperty(target, key) - return typeof reflect == 'function' ? reflect.bind(target) : reflect - }, - } - - this.#expireMap = new Proxy(this.#expireMap, handler) - } - - /** - * get 获取数据 - * @param {string} key 键 - * @returns {Promise|Error} 数据 - */ - async get (key) { - try { - /** 先查过期时间 */ - const expire = this.#expireMap.get(key) - if (expire && expire < Date.now()) { - await this.#level.del(key) - this.#expireMap.delete(key) - return null - } - - return await this.#level.get(key) - } catch (error) { - if (error.notFound) return null - throw error - } - } - - /** - * set 设置数据 - * @param {string} key 键 - * @param {string} value 值 - * @param {object} [options] 选项 - * @param {number} [options.EX] 过期时间 单位秒 - * @returns {Promise|Error} - */ - async set (key, value, options) { - if (options && options.EX) { - this.#expireMap.set(key, Date.now() + options.EX * 1000) - } - - return await this.#level.put(key, value) - } - - /** - * del 删除数据 - * @param {string} key 键 - * @returns {Promise|Error} - */ - async del (key) { - this.#expireMap.delete(key) - return await this.#level.del(key) - } - - /** - * keys 获取所有键 - * @param {string} [prefix] 前缀 - * @returns {Promise|Error} 键列表 - */ - async keys (prefix = '') { - const keys = [] - for await (const key of this.#level.keys({ gte: prefix, lt: prefix + '\xFF' })) { - // Check if the key has expired - const expire = this.#expireMap.get(key) - if (expire && expire < Date.now()) { - await this.#level.del(key) - this.#expireMap.delete(key) - } else { - keys.push(key) - } - } - return keys - } - - /** - * incr 递增数据 - * @param {string} key 键 - * @returns {Promise|Error} - */ - async incr (key) { - let value = await this.get(key) - if (value === null) { - value = 0 - } else { - value = Number(value) - if (isNaN(value)) throw new Error('Value is not a number') - } - value += 1 - await this.set(key, value.toString()) - return value - } - - /** - * decr 递减数据 - * @param {string} key 键 - * @returns {Promise|Error} - */ - async decr (key) { - let value = await this.get(key) - if (value === null) { - value = 0 - } else { - value = Number(value) - if (isNaN(value)) throw new Error('Value is not a number') - } - value -= 1 - await this.set(key, value.toString()) - return value - } - - /** - * expire 设置过期时间 - * @param {string} key 键 - * @param {number} seconds 过期时间 单位秒 - * @returns {Promise|Error} - */ - async expire (key, seconds) { - this.#expireMap.set(key, Date.now() + seconds * 1000) - return seconds - } - - /** - * ttl 获取过期时间 - * @param {string} key 键 - * @returns {Promise|Error} - */ - async ttl (key) { - const expire = this.#expireMap.get(key) - if (expire) { - return Math.ceil((expire - Date.now()) / 1000) - } - return -2 - } - - /** - * setEx 设置数据并设置过期时间 - * @param {string} key 键 - * @param {number} seconds 过期时间 单位秒 - * @param {string} value 值 - * @returns {Promise|Error} - */ - async setEx (key, seconds, value) { - this.#expireMap.set(key, Date.now() + seconds * 1000) - return await this.#level.put(key, value) - } - - /** - * exists 判断键是否存在 - * @param {string} key 键 - * @returns {Promise|Error} - */ - async exists (key) { - const value = await this.get(key) - return value === null ? 0 : 1 - } - - /** - * zAdd 有序集合添加元素 - * @param {string} key 键 - * @param {object} data 数据 - * @param {number} data.score 分数 - * @param {string} data.value 值 - */ - async zAdd (key, { score, value }) { - let set = await this.get(key) - if (set === null) { - set = [] - } else { - set = JSON.parse(set) - } - set.push({ score, value }) - set.sort((a, b) => a.score - b.score) - await this.set(key, JSON.stringify(set)) - } - - /** - * zRem 有序集合删除元素 - * @param {string} key 键 - * @param {string} value 值 - */ - async zRem (key, value) { - let set = await this.get(key) - if (set === null) { - return - } - set = JSON.parse(set) - set = set.filter(item => item.value !== value) - await this.set(key, JSON.stringify(set)) - } - - /** - * zIncrBy 有序集合分数递增 - * @param {string} key 键 - * @param {number} increment 递增值 - * @param {string} value 值 - * @returns {Promise|Error} - */ - async zIncrBy (key, increment, value) { - let set = await this.get(key) - if (set === null) { - throw new Error('Set does not exist') - } - set = JSON.parse(set) - const item = set.find(item => item.value === value) - if (item) { - item.score += increment - } - set.sort((a, b) => a.score - b.score) - await this.set(key, JSON.stringify(set)) - return item.score - } - - /** - * zRangeByScore 有序集合根据分数范围获取元素 - * @param {string} key 键 - * @param {number} min 最小分数 - * @param {number} max 最大分数 - * @returns {Promise|Error} - */ - async zRangeByScore (key, min, max) { - let set = await this.get(key) - if (set === null) { - return [] - } - set = JSON.parse(set) - return set.filter(item => item.score >= min && item.score <= max).map(item => item.value) - } - - /** - * zScore 有序集合获取元素分数 - * @param {string} key 键 - * @param {string} value 值 - * @returns {Promise|Error} - */ - async zScore (key, value) { - let set = await this.get(key) - if (set === null) { - return null - } - set = JSON.parse(set) - const item = set.find(item => item.value === value) - return item ? item.score : null - } -} diff --git a/lib/event/event.js b/lib/event/event.js deleted file mode 100644 index 92f7c4e..0000000 --- a/lib/event/event.js +++ /dev/null @@ -1,193 +0,0 @@ -/* eslint-disable no-unused-vars */ -import Review from './review.js' -import { logger, segment, Bot, common, Cfg } from '#Karin' -import { KarinEvent } from '../bot/KarinEvent.js' -import { KarinMessage } from '../bot/KarinMessage.js' -import { KarinNotice } from '../bot/KarinNotice.js' -import { KarinRequest } from '../bot/KarinRequest.js' - -export default class Event { - /** - * 处理事件,加入自定义字段 - * @param {KarinEvent|KarinMessage|KarinNotice|KarinRequest} e - 包含消息信息的对象 - */ - constructor (e) { - this.e = e - this.e.isAdmin = false - this.e.isMaster = false - this.e.isPrivate = false - this.e.isGroup = false - this.e.isGuild = false - this.e.logText = '' - this.e.logFnc = '' - this.e.store = new Map() - this.config = {} - /** 加入e.bot */ - Object.defineProperty(this.e, 'bot', { value: Bot.getBot(e.self_id) }) - if (this.e.group_id) this.config = Cfg.group(e.group_id) - } - - /** - * 事件处理 - */ - review () { - /** 检查CD */ - if (!Review.CD(this.e, this.config)) { - logger.debug('[消息拦截] 正在冷却中') - return true - } - - /** 检查群聊黑白名单 */ - if (!Review.GroupEnable(this.e)) { - logger.debug('[消息拦截] 未通过群聊黑白名单检查') - return true - } - - /** 检查用户黑白名单 */ - if (!Review.UserEnable(this.e)) { - logger.debug('[消息拦截] 未通过用户黑白名单检查') - return true - } - - /** 都通过了 */ - return false - } - - /** - * 根据事件类型过滤事件 - * @param {string} event - 验证对象 - * @returns {boolean} - 是否匹配事件 - */ - filtEvent (event) { - /** 事件映射表 */ - const eventMap = { - message: () => `message.${this.e.sub_event}`, - notice: () => `notice.${this.e.sub_event}`, - request: () => `request.${this.e.sub_event}`, - } - - const eventType = eventMap[this.e.event]() - return eventType.includes(event) - } - - /** - * 判断权限 - * @param {'all'|'master'|'admin'|'group.owner'|'group.admin'} permission - 权限 - * @returns {boolean} - 是否有权限 - */ - filterPermission (permission) { - if (permission === 'all' || !permission) return true - - if (permission === 'master') { - if (!this.e.isMaster) { - this.e.reply('暂无权限,只有主人才能操作') - return false - } - return true - } - - if (permission === 'admin') { - if (!this.e.isMaster && !this.e.isAdmin) { - this.e.reply('暂无权限,只有管理员才能操作') - return false - } - return true - } - - if (this.e.isGroup) { - const list = { - 'group.owner': { - role: 'owner', - name: '群主', - }, - 'group.admin': { - role: 'admin', - name: '群管理员', - }, - - } - - const role = list[permission] - if (role && this.e.sender?.role !== role.role) { - this.e.reply(`暂无权限,只有${role.name}才能操作`) - return false - } - } - - return true - } - - /** - * 快速回复 - */ - reply () { - /** - * @param {string|object|Array} elements - 发送的消息 - * @param {object} options - 回复数据 - * @param {boolean} options.at - 是否at用户 - * @param {boolean} options.reply - 是否引用回复 - * @param {number} options.recallMsg - 群聊是否撤回消息,0-120秒,0不撤回 - * @param {boolean} options.button - 是否使用按钮 - * @param {number} options.retry_count - 重试次数 - * @returns {Promise<{ message_id?: string }>} - 返回消息ID - */ - this.e.reply = async (elements = '', options = { reply: false, recallMsg: 0, at: false, button: false, retry_count: 1 }) => { - /** 将msg格式化为数组 */ - if (!Array.isArray(elements)) { - elements = [elements] - } - elements = elements.map(element => { - if (typeof element === 'string') { - return segment.text(element) - } - return element - }) - const { reply, recallMsg, at, button, retry_count } = options - logger.debug(button) // 后续处理 - /** 加入at */ - if (at && this.e.isGroup) elements.unshift(segment.at(this.e.user_id)) - /** 加入引用回复 */ - if (reply && this.e.message_id) elements.unshift(segment.reply(this.e.message_id)) - - /** 先发 提升速度 */ - let msgRes = this.e.replyCallback(elements, retry_count) - const reply_log = common.makeMessageLog(elements) - - if (this.e.isGroup) { - Review.GroupMsgPrint(this.e) && logger.bot('info', this.e.self_id, `${logger.green(`Send Group ${this.e.group_id}: `)}${reply_log}`) - } else { - logger.bot('info', this.e.self_id, `${logger.green(`Send private ${this.e.user_id}: `)}${reply_log}`) - } - - try { - Bot.emit('karin:count:send', 1) - /** 取结果 */ - msgRes = await msgRes - logger.bot('debug', this.e.self_id, `回复消息结果:${JSON.stringify(msgRes)}`) - } catch (error) { - logger.bot('error', this.e.self_id, `回复消息失败:${reply_log}`) - logger.bot('error', this.e.self_id, error.stack || error.message || JSON.stringify(error)) - } - - /** 快速撤回 */ - if (recallMsg > 0 && msgRes?.message_id) { - setTimeout(() => this.e.bot.RecallMessage(null, msgRes.message_id), recallMsg * 1000) - } - - return msgRes - } - Object.freeze(this.e.reply) - } - - /** - * @type {{ - * GroupCD: number, - * GroupUserCD: number, - * mode: '0'|'1'|'2'|'3'|'4'|'5', - * alias: string[], - * enable: string[], - * disable: string[] - * }} - */ - config -} diff --git a/lib/event/message.js b/lib/event/message.js deleted file mode 100644 index c086b5a..0000000 --- a/lib/event/message.js +++ /dev/null @@ -1,259 +0,0 @@ -import lodash from 'lodash' -import util from 'util' -import Event from './event.js' -import Review from './review.js' -import loader from '../plugins/loader.js' -import { Bot, Cfg, logger } from '#Karin' -import { stateArr } from '../plugins/plugin.js' - -/** 消息事件 */ -export default class Message extends Event { - constructor (e) { - super(e) - Bot.emit('karin:count:recv', 1) - /** 处理消息 保证日志的打印 */ - this.dealMsg() - /** 事件处理 */ - if (this.review()) return - - /** 响应模式 */ - if (this.e.group_id && this.config.mode && !Review.mode(this.e, this.config)) { - logger.debug('[消息拦截] 响应模式不匹配') - return - } - /** 处理回复 */ - this.reply() - /** 处理消息 */ - this.deal() - } - - /** - * 处理消息 - */ - async deal () { - /** 上下文 */ - if (await this.context(this.e)) return - - /* eslint-disable no-labels */ - a: - for (const app of loader.Apps) { - /** 判断事件 */ - if (app.event && !this.filtEvent(app.event)) continue - - /** 判断权限 */ - if (!this.filterPermission(app.permission)) break a - - /** 正则匹配 */ - for (const v of app.rule) { - /** 这里的lastIndex是为了防止正则无法从头开始匹配 */ - v.reg.lastIndex = 0 - if (v.reg.test(this.e.msg)) { - /** 检查黑白名单插件 */ - if (!Review.PluginEnable(app, this.config)) continue - - /** 判断子事件 */ - if (v.event && !this.filtEvent(v.event)) continue - - this.e.logFnc = `[${app.file.dir}][${app.name}][${v.fnc}]` - const logFnc = logger.fnc(`[${app.name}][${v.fnc}]`) - this.GroupMsgPrint && v.log(this.e.self_id, `${logFnc}${this.e.logText} ${lodash.truncate(this.e.msg, { length: 80 })}`) - - /** 判断权限 */ - if (!this.filterPermission(v.permission)) break a - - try { - /** - * 插件对象 - * @param {any} App - 插件对象 - * @param {EventType.Message} App.e - 事件对象 - */ - const App = new app.App() - App.e = this.e - let res = App[v.fnc] && App[v.fnc](this.e) - - /** 计算插件处理时间 */ - const start = Date.now() - Bot.emit('karin:count:fnc', this.e.logFnc) - if (util.types.isPromise(res)) res = await res - this.GroupMsgPrint && v.log(this.e.self_id, `${logFnc} ${lodash.truncate(this.e.msg, { length: 80 })} 处理完成 ${logger.green(Date.now() - start + 'ms')}`) - if (res !== false) break a - } catch (error) { - logger.error(`${this.e.logFnc}`) - logger.error(error.stack || error.message || JSON.stringify(error)) - break a - } - } - } - } - } - - /** - * 处理消息体 - */ - dealMsg () { - const logs = [] - for (const val of this.e.elements) { - switch (val.type) { - case 'text': { - const msg = (val.text || '').replace(/^\s*[#井#]+\s*/, '#').replace(/^\s*[\\*※*]+\s*/, '*').trim() - this.e.msg += msg - /** 美观一点... */ - logs.push(msg) - break - } - case 'face': - logs.push(`[face:${val.id}]`) - break - case 'video': - case 'record': - logs.push(`[${val.type}:${val.file}]`) - break - case 'image': - this.e.image.push(val.file) - logs.push(`[image:${val.file}]`) - break - case 'file': - this.e.file = val - logs.push(`[file:${val.file}]`) - break - case 'at': - /** atBot不计入e.at */ - // eslint-disable-next-line eqeqeq - if (val.uid && val.uid == this.e.bot.account.uid) { - this.e.atBot = true - // eslint-disable-next-line eqeqeq - } else if (val.uin == this.e.bot.account.uin) { - this.e.atBot = true - } else if (val.uid && val.uid === 'all') { - this.e.atAll = true - } else { - this.e.at.push(val.uid || val.uin) - } - logs.push(`[at:${val.uid || val.uin}]`) - break - case 'rps': - logs.push(`[rps:${val.id}]`) - break - case 'dice': - logs.push(`[dice:${val.id}]`) - break - case 'shake': - logs.push('[shake: 窗口抖动]') - break - case 'poke': - logs.push(`[poke:${val.id}]`) - break - case 'anonymous': - logs.push(`[anonymous:${val.id}]`) - break - case 'share': - logs.push(`[share:${val.url}]`) - break - case 'contact': - logs.push(`[contact:${val.id}]`) - break - case 'location': - logs.push(`[location:${val.lat}-${val.lon}]`) - break - case 'music': - logs.push(`[music:${JSON.stringify(val)}]`) - break - case 'reply': - this.e.reply_id = val.message_id - logs.push(`[reply:${val.message_id}]`) - break - case 'forward': - logs.push(`[forward:${val.id}]`) - break - case 'node': - logs.push(`[node:${JSON.stringify(val)}]`) - break - case 'xml': - this.e.msg += val.data - logs.push(`[xml:${val.data}]`) - break - case 'json': - this.e.msg += val.data - logs.push(`[json:${JSON.stringify(val.data)}]`) - break - case 'markdown': { - if (val.content) { - logs.push(`[markdown:${val.markdown}]`) - } else { - const content = { id: val.custom_template_id } - for (const v of val.params) content[v.key] = v.values[0] - logs.push(`[markdown:${JSON.stringify(content)}]`) - } - break - } - case 'rows': { - const rows = [] - for (const v of val.rows) rows.push(v.log) - logs.push(`[rows:${JSON.stringify(rows)}]`) - break - } - case 'button': - logs.push(`[button:${val.log}]`) - break - default: - logs.push(`[未知:${JSON.stringify(val)}]`) - } - } - this.e.raw_message = logs.join('') - - /** 前缀处理 */ - this.e.group_id && Review.alias(this.e, this.config) - - /** 主人 */ - if (Cfg.master.includes(Number(this.e.user_id) || String(this.e.user_id))) { - this.e.isMaster = true - this.e.isAdmin = true - } else if (Cfg.admin.includes(Number(this.e.user_id) || String(this.e.user_id))) { - /** 管理员 */ - this.e.isAdmin = true - } - - this.GroupMsgPrint = false - if (this.e.contact.scene === 'friend') { - this.e.isPrivate = true - this.e.logText = `[Private:${this.e.sender.nick || ''}(${this.e.user_id})]` - logger.bot('info', this.e.self_id, `私聊:[${this.e.user_id}(${this.e.sender.nick || ''})] ${this.e.raw_message}`) - } else if (this.e.contact.scene === 'group') { - this.e.isGroup = true - this.e.logText = `[Group:${this.e.group_id}-${this.e.user_id}(${this.e.sender.nick || ''})]` - this.GroupMsgPrint = Review.GroupMsgPrint(this.e) - this.GroupMsgPrint && logger.bot('info', this.e.self_id, `群消息:[${this.e.group_id}-${this.e.user_id}(${this.e.sender.nick || ''})] ${this.e.raw_message}`) - } else { - logger.bot('info', this.e.self_id, `未知消息:${JSON.stringify(this.e)}`) - } - } - - /** - * 处理上下文 - * @returns {Promise} 是否有上下文 - */ - async context () { - const key = this.e.isGroup ? `${this.e.group_id}.${this.e.user_id}` : this.e.user_id - const App = stateArr[key] - if (App) { - const { plugin, fnc } = App - /** 计算插件处理时间 */ - const start = Date.now() - let res - this.e.store = plugin.e.store - if (fnc instanceof Function) { - this.e.logFnc = `[${plugin.name}][function]` - plugin.e = this.e - res = fnc(this.e) - } else { - this.e.logFnc = `[${plugin.name}][${fnc}]` - plugin.e = this.e - res = plugin[fnc] && plugin[fnc](this.e) - } - if (util.types.isPromise(res)) res = await res - logger.bot('mark', this.e.self_id, `${this.e.logFnc} 上下文处理完成 ${Date.now() - start}ms`) - return true - } - return false - } -} diff --git a/lib/event/notice.js b/lib/event/notice.js deleted file mode 100644 index d5fc171..0000000 --- a/lib/event/notice.js +++ /dev/null @@ -1,211 +0,0 @@ -import util from 'util' -import lodash from 'lodash' -import Event from './event.js' -import Review from './review.js' -import { Cfg, logger } from '#Karin' -import loader from '../plugins/loader.js' - -/** - * 通知事件 - */ -export default class Notice extends Event { - constructor (e) { - super(e) - /** 事件处理 */ - if (this.review()) return - /** 处理回复 */ - this.reply() - /** raw */ - this.raw_message() - /** 处理消息 */ - this.deal() - } - - /** - * 处理事件 - */ - async deal () { - /** 主人 */ - if (Cfg.master.includes(Number(this.e.user_id) || String(this.e.user_id))) this.e.isMaster = true - /** 管理员 */ - if (Cfg.admin.includes(Number(this.e.user_id) || String(this.e.user_id))) this.e.isAdmin = true - this.GroupMsgPrint = false - if (this.e.contact.scene === 'friend') { - this.e.isPrivate = true - this.e.logText = `[Private:${this.e.sender.nick || ''}(${this.e.user_id})]` - logger.bot('info', this.e.self_id, `${logger.green('私聊通知: ')}[${this.e.user_id}(${this.e.sender.nick || ''})] ${this.e.raw_message}`) - } else if (this.e.contact.scene === 'group') { - this.e.isGroup = true - this.e.logText = `[Group:${this.e.group_id}-${this.e.user_id}(${this.e.sender.nick || ''})]` - this.GroupMsgPrint = Review.GroupMsgPrint(this.e) - this.GroupMsgPrint && logger.bot('info', this.e.self_id, `${logger.green('群通知: ')}[${this.e.group_id}-${this.e.user_id}(${this.e.sender.nick || ''})] ${this.e.raw_message}`) - } else { - logger.bot('info', this.e.self_id, `未知来源通知事件:${JSON.stringify(this.e)}`) - } - /* eslint-disable no-labels */ - a: - for (const app of loader.Apps) { - /** 判断事件 */ - if (app.event && !this.filtEvent(this.e, app.event)) continue - - /** accept hook */ - if (app.accept) { - /** 检查黑白名单插件 */ - if (!Review.PluginEnable(app, this.config)) continue - - /** 日志方法字符串 */ - this.e.logFnc = `[${app.file.dir}][${app.name}][accept]` - const logFnc = logger.fnc(`[${app.name}][accept]`) - - /** 判断权限 */ - if (!this.filterPermission(app.filterPermission)) break a - - try { - /** 实例化 */ - const App = new app.App() - App.e = this.e - let res = App.accept && App.accept(this.e) - - /** 计算插件处理时间 */ - const start = Date.now() - - if (util.types.isPromise(res)) res = await res - - if (res !== false) { - this.GroupMsgPrint && logger.bot('info', this.e.self_id, `${logFnc} ${lodash.truncate(this.e.msg, { length: 80 })} 处理完成 ${logger.green(Date.now() - start + 'ms')}`) - break a - } - } catch (error) { - logger.error(`${logFnc}`) - logger.error(error.stack || error.message || JSON.stringify(error)) - break a - } - } - } - } - - /** - * 构建原始消息 - */ - raw_message () { - switch (this.e.sub_event) { - /** 好友头像戳一戳 */ - case 'friend_poke': { - this.e.raw_message = '[好友戳一戳]: 戳了你一下' - break - } - /** 好友消息撤回 */ - case 'friend_recall': { - this.e.raw_message = `[好友消息撤回]: ${this.e.message_id}` - break - } - /** 私聊文件上传 */ - case 'friend_file': { - const { url } = this.e.content - this.e.raw_message = `[私聊文件上传]: ${url}` - break - } - /** 群头像戳一戳 */ - case 'group_poke': { - const { operator_uid, operator_uin, target_uid, target_uin } = this.e.content - this.e.raw_message = `[群戳一戳]: ${operator_uid || operator_uin} 戳了戳 ${target_uid || target_uin}` - break - } - /** 群消息撤回 */ - case 'group_recall': { - const { operator_uid, operator_uin, message_id } = this.e.content - this.e.raw_message = `[群消息撤回]: ${operator_uid || operator_uin} 撤回了一条消息 ${message_id}` - break - } - /** 群文件上传 */ - case 'group_file_uploaded': { - const { url, operator_uid, operator_uin } = this.e.content - this.e.raw_message = `[群文件上传]: ${operator_uid || operator_uin} 上传了 ${url}` - break - } - /** 群成员增加 */ - case 'group_member_increase': { - // 这里可能不准确...没测试 - // eslint-disable-next-line no-unused-vars - const { operator_uid, operator_uin, target_uid, target_uin } = this.e.content - this.e.raw_message = `[群成员新增]: ${target_uid || target_uin} 加入群聊` - break - } - /** 群成员减少 */ - case 'group_member_decrease': { - switch (this.e.content.type) { - // 主动退群 - case 0: { - const { target_uid, target_uin } = this.e.content - this.e.raw_message = `[群成员减少]: ${target_uid || target_uin} 退出群聊` - break - } - // 群成员被踢 - case 1: { - const { operator_uid, operator_uin, target_uid, target_uin } = this.e.content - this.e.raw_message = `[群成员减少]: ${operator_uid || operator_uin} 将 ${target_uid || target_uin} 踢出群聊` - break - } - // bot被踢 - case 2: { - const { operator_uid, operator_uin } = this.e.content - this.e.raw_message = `[群成员减少]: 机器人被移除群聊,操作人:${operator_uid || operator_uin}` - break - } - } - break - } - /** 群管理员变动 */ - case 'group_admin_change': { - const { target_uid, target_uin, is_admin } = this.e.content - this.e.raw_message = `[群管理员变动]: ${target_uid || target_uin} 被${is_admin ? '设置' : '取消'}群管理员` - break - } - /** 群成员被禁言 */ - case 'group_member_ban': { - const { operator_uid, operator_uin, target_uid, target_uin, type } = this.e.content - this.e.raw_message = `[群成员禁言]: ${operator_uid || operator_uin} ${type === 1 ? '禁言' : '解禁'}了 ${target_uid || target_uin}` - break - } - /** 群签到 */ - case 'group_sign_in': { - const { target_uid, target_uin } = this.e.content - this.e.raw_message = `[群签到]: ${target_uid || target_uin}` - break - } - /** 群全员禁言 */ - case 'group_whole_ban': { - const { operator_uid, operator_uin, is_ban } = this.e.content - this.e.raw_message = `[群全员禁言]: ${operator_uid || operator_uin} ${is_ban ? '开启全员禁言' : '解除全员禁言'}` - break - } - /** 群名片改变 */ - case 'group_card_changed': { - const { operator_uid, operator_uin, target_uid, target_uin, new_card } = this.e.content - this.e.raw_message = `[群名片改变]: ${operator_uid || operator_uin} 修改了 ${target_uid || target_uin} 的名片为 ${new_card}` - break - } - /** 群成员专属头衔改变 */ - case 'group_member_unique_title_changed': { - const { target, title } = this.e.content - this.e.raw_message = `[群头衔更改]: ${target} 的专属头衔改变为 ${title}` - break - } - /** 群精华消息改变 */ - case 'group_essence_changed': { - const { operator_uid, operator_uin, target_uid, target_uin, message_id, is_set } = this.e.content - this.e.raw_message = `[群精华消息]: ${operator_uid || operator_uin} ${is_set ? `将${target_uid || target_uin}的消息${message_id}设置为精华消息` : `取消了${target_uid || target_uin}精华消息 ${message_id}`}` - break - } - /** 群表情回应 */ - case 'group_message_reaction': { - const { operator_uid, operator_uin, message_id, face_id } = this.e.content - this.e.raw_message = `[群表情回应]: ${operator_uid || operator_uin} 给消息 ${message_id} 回应了一个${face_id}的表情` - break - } - default: { - this.e.raw_message = `[未知事件]: ${JSON.stringify(this.e)}` - } - } - } -} diff --git a/lib/event/request.js b/lib/event/request.js deleted file mode 100644 index 7fdf7d4..0000000 --- a/lib/event/request.js +++ /dev/null @@ -1,84 +0,0 @@ -import util from 'util' -import lodash from 'lodash' -import Event from './event.js' -import Review from './review.js' -import { Cfg, logger } from '#Karin' -import loader from '../plugins/loader.js' - -/** - * 请求事件 - */ -export default class Request extends Event { - constructor (e) { - super(e) - /** 事件处理 */ - if (this.review()) return - /** 处理回复 */ - this.reply() - /** 处理消息 */ - this.deal() - } - - /** - * 处理事件 - */ - async deal () { - /** 主人 */ - if (Cfg.master.includes(Number(this.e.user_id) || String(this.e.user_id))) this.e.isMaster = true - /** 管理员 */ - if (Cfg.admin.includes(Number(this.e.user_id) || String(this.e.user_id))) this.e.isAdmin = true - this.GroupMsgPrint = false - if (this.e.contact.scene === 'friend') { - this.e.isPrivate = true - this.e.logText = `[Private:${this.e.sender.nick || ''}(${this.e.user_id})]` - logger.bot('info', this.e.self_id, `私聊请求事件:[${this.e.user_id}(${this.e.sender.nick || ''})] ${this.e.raw_message}`) - } else if (this.e.contact.scene === 'group') { - this.e.isGroup = true - this.e.logText = `[Group:${this.e.group_id}-${this.e.user_id}(${this.e.sender.nick || ''})]` - this.GroupMsgPrint = Review.GroupMsgPrint(this.e) - this.GroupMsgPrint && logger.bot('info', this.e.self_id, `群请求事件:[${this.e.group_id}-${this.e.user_id}(${this.e.sender.nick || ''})] ${this.e.raw_message}`) - } else { - logger.bot('info', this.e.self_id, `未知来源请求事件:${JSON.stringify(this.e)}`) - } - /* eslint-disable no-labels */ - a: - for (const app of loader.Apps) { - /** 判断事件 */ - if (app.event && !this.filtEvent(app.event)) continue - - /** accept hook */ - if (app.accept) { - /** 检查黑白名单插件 */ - if (!Review.PluginEnable(app, this.config)) continue - - /** 日志方法字符串 */ - this.e.logFnc = `[${app.file.dir}][${app.name}][accept]` - const logFnc = logger.fnc(`[${app.name}][accept]`) - - /** 判断权限 */ - if (!this.filterPermission(app.filterPermission)) break a - - try { - /** 实例化 */ - const App = new app.App() - App.e = this.e - let res = App.accept && App.accept(this.e) - - /** 计算插件处理时间 */ - let start = Date.now() - - if (util.types.isPromise(res)) res = await res - - if (res !== false) { - this.GroupMsgPrint && logger.bot('info', this.e.self_id, `${logFnc} ${lodash.truncate(this.e.msg, { length: 80 })} 处理完成 ${logger.green(Date.now() - start + 'ms')}`) - break a - } - } catch (error) { - logger.error(`${logFnc}`) - logger.error(error.stack || error.message || JSON.stringify(error)) - break a - } - } - } - } -} diff --git a/lib/event/review.js b/lib/event/review.js deleted file mode 100644 index ec183aa..0000000 --- a/lib/event/review.js +++ /dev/null @@ -1,388 +0,0 @@ -import { Cfg } from '#Karin' - -/** - * 事件拦截器 - * 利用可执行函数的特性,热更新所有拦截器 - * 所有拦截器返回的都是布尔值 为true说明通过 为false则未通过 - */ -class Review { - constructor () { - /** 群聊所有消息cd */ - this.GroupCD = {} - /** 群聊个人cd */ - this.GroupUserCD = {} - - /** 事件cd */ - this.CD = (e, config) => true - /** 响应模式 */ - this.mode = (e, config) => true - /** 前缀、别名 */ - this.alias = (e, config) => true - /** 群聊黑白名单 哪个群可以触发事件 */ - this.GroupEnable = (e) => true - /** 用户黑白名单 谁可以触发事件 */ - this.UserEnable = (e) => true - /** 群聊事件日志 哪个群可以打印日志 */ - this.GroupMsgPrint = (e) => true - /** 插件黑白名单 哪个插件可以被触发 */ - this.PluginEnable = (app, config) => true - - // 延迟1秒执行 - setTimeout(() => { - this.main() - }, 1000) - } - - main () { - this.App = Cfg.App - this.Config = Cfg.Config - this.#CD() - this.#mode() - this.#alias() - this.#GroupEnable() - this.#UserEnable() - this.#GroupMsgPrint() - this.#PluginEnable() - } - - /** - * 群聊黑白名单 允许哪个群触发事件 - */ - #GroupEnable () { - /** 同时启用 */ - if (this.App.WhiteList.groups && this.App.BlackList.groups) { - this.GroupEnable = (e) => { - /** 白名单不为空 */ - if (Array.isArray(this.Config.WhiteList.groups) && this.Config.WhiteList.groups.length) { - return this.Config.WhiteList.groups.includes(Number(e.group_id) || String(e.group_id)) - } - - /** 白名单为空 检查黑名单是否为空 */ - if (Array.isArray(this.Config.BlackList.groups) && this.Config.BlackList.groups.length) { - return !this.Config.BlackList.groups.includes(Number(e.group_id) || String(e.group_id)) - } - - /** 黑白名单都为空 */ - return true - } - return true - } - - /** 白名单启用 */ - if (this.App.WhiteList.groups) { - this.GroupEnable = (e) => { - if (Array.isArray(this.Config.WhiteList.groups) && this.Config.WhiteList.groups.length) { - return this.Config.WhiteList.groups.includes(Number(e.group_id) || String(e.group_id)) - } - return true - } - return true - } - - /** 黑名单启用 */ - if (this.App.BlackList.groups) { - this.GroupEnable = (e) => { - if (Array.isArray(this.Config.BlackList.groups) && this.Config.BlackList.groups.length) { - return !this.Config.BlackList.groups.includes(Number(e.group_id) || String(e.group_id)) - } - return true - } - return true - } - - /** 都没有启用 */ - this.GroupEnable = () => true - } - - /** - * 用户黑白名单 允许那个用户触发事件 - */ - #UserEnable () { - /** 同时启用 */ - if (this.App.WhiteList.users && this.App.BlackList.users) { - this.UserEnable = (e) => { - /** 白名单不为空 */ - if (Array.isArray(this.Config.WhiteList.users) && this.Config.WhiteList.users.length) { - return this.Config.WhiteList.users.includes(Number(e.user_id) || String(e.user_id)) - } - - /** 白名单为空 检查黑名单是否为空 */ - if (Array.isArray(this.Config.BlackList.users) && this.Config.BlackList.users.length) { - return !this.Config.BlackList.users.includes(Number(e.user_id) || String(e.user_id)) - } - - /** 黑白名单都为空 */ - return true - } - return true - } - - /** 白名单启用 */ - if (this.App.WhiteList.users) { - this.UserEnable = (e) => { - if (Array.isArray(this.Config.WhiteList.users) && this.Config.WhiteList.users.length) { - return this.Config.WhiteList.users.includes(Number(e.user_id) || String(e.user_id)) - } - return true - } - return true - } - - /** 黑名单启用 */ - if (this.App.BlackList.users) { - this.UserEnable = (e) => { - if (Array.isArray(this.Config.BlackList.users) && this.Config.BlackList.users.length) { - return !this.Config.BlackList.users.includes(Number(e.user_id) || String(e.user_id)) - } - return true - } - return true - } - - /** 都没有启用 */ - this.UserEnable = () => true - } - - /** - * 群聊事件日志 是否打印 - */ - #GroupMsgPrint () { - /** 同时启用 */ - if (this.App.WhiteList.GroupMsgLog && this.App.BlackList.GroupMsgLog) { - this.GroupMsgPrint = (e) => { - /** 白名单不为空 */ - if (Array.isArray(this.Config.WhiteList.GroupMsgLog) && this.Config.WhiteList.GroupMsgLog.length) { - return this.Config.WhiteList.GroupMsgLog.includes(Number(e.group_id) || String(e.group_id)) - } - - /** 白名单为空 检查黑名单是否为空 */ - if (Array.isArray(this.Config.BlackList.GroupMsgLog) && this.Config.BlackList.GroupMsgLog.length) { - return !this.Config.BlackList.GroupMsgLog.includes(Number(e.group_id) || String(e.group_id)) - } - - /** 黑白名单都为空 */ - return true - } - return true - } - - /** 白名单启用 */ - if (this.App.WhiteList.GroupMsgLog) { - this.GroupMsgPrint = (e) => { - if (Array.isArray(this.Config.WhiteList.GroupMsgLog) && this.Config.WhiteList.GroupMsgLog.length) { - return this.Config.WhiteList.GroupMsgLog.includes(Number(e.group_id) || String(e.group_id)) - } - return true - } - return true - } - - /** 黑名单启用 */ - if (this.App.BlackList.GroupMsgLog) { - this.GroupMsgPrint = (e) => { - if (Array.isArray(this.Config.BlackList.GroupMsgLog) && this.Config.BlackList.GroupMsgLog.length) { - return !this.Config.BlackList.GroupMsgLog.includes(Number(e.group_id) || String(e.group_id)) - } - return true - } - return true - } - - /** 都没有启用 */ - this.GroupMsgPrint = () => true - } - - /** - * 黑白名单插件 哪个插件可以被触发 - */ - #PluginEnable () { - /** 同时启用 */ - if (this.App.GroupConfig.enable && this.App.GroupConfig.disable) { - this.PluginEnable = (app, config) => { - /** 白名单不为空 */ - if (Array.isArray(config.enable) && config.enable.length) { - /** 插件包是否处于功能白名单 */ - if (config.enable.includes(app.file.dir)) return true - /** 插件是否处于功能白名单 */ - if (config.enable.includes(`${app.file.dir}/${app.file.name}`)) return true - /** 插件名称是否处于功能白名单 */ - if (config.enable.includes(app.name)) return true - logger.debug(logger.green(`[功能白名单] 插件名称 [${app.name}] 不存在功能白名单中`)) - return false - } - - /** 白名单为空 检查黑名单是否为空 */ - if (Array.isArray(config.disable) && config.disable.length) { - /** 插件包是否处于功能黑名单 */ - if (config.disable.includes(app.file.dir)) { - logger.debug(logger.red(`[功能黑名单] 插件包 [${app.file.dir}] 处于功能黑名单`)) - return false - } - /** 插件是否处于功能黑名单 */ - if (config.disable.includes(`${app.file.dir}/${app.file.name}`)) { - logger.debug(logger.red(`[功能黑名单] 插件 [${app.file.dir}/${app.file.name}] 处于功能黑名单`)) - return false - } - /** 插件名称是否处于功能黑名单 */ - if (config.disable.includes(app.name)) { - logger.debug(logger.red(`[功能黑名单] 插件名称 [${app.name}] 处于功能黑名单`)) - return false - } - return true - } - - /** 黑白名单都为空 */ - return true - } - return true - } - - /** 白名单启用 */ - if (this.App.GroupConfig.enable) { - this.PluginEnable = (app, config) => { - if (Array.isArray(config.enable) && config.enable.length) { - if (config.enable.includes(app.file.dir)) return true - if (config.enable.includes(`${app.file.dir}/${app.file.name}`)) return true - if (config.enable.includes(app.name)) return true - logger.debug(logger.green(`[功能白名单] 插件名称 [${app.name}] 不存在功能白名单中`)) - return false - } - return true - } - return true - } - - /** 黑名单启用 */ - if (this.App.GroupConfig.disable) { - this.PluginEnable = (app, config) => { - if (Array.isArray(config.disable) && config.disable.length) { - if (config.disable.includes(app.file.dir)) { - logger.debug(logger.red(`[功能黑名单] 插件包 [${app.file.dir}] 处于功能黑名单`)) - return false - } - if (config.disable.includes(`${app.file.dir}/${app.file.name}`)) { - logger.debug(logger.red(`[功能黑名单] 插件 [${app.file.dir}/${app.file.name}] 处于功能黑名单`)) - return false - } - if (config.disable.includes(app.name)) { - logger.debug(logger.red(`[功能黑名单] 插件名称 [${app.name}] 处于功能黑名单`)) - return false - } - return true - } - return true - } - return true - } - - /** 都没有启用 */ - this.PluginEnable = () => true - return true - } - - /** 群聊cd */ - #CD () { - /** 同时启用 */ - if (this.App.GroupConfig.GroupCD && this.App.GroupConfig.GroupUserCD) { - this.CD = (e, config) => { - const key = `${e.group_id}.${e.user_id}` - /** cd中... */ - if (this.GroupCD[e.group_id] || this.GroupUserCD[key]) return false - - /** 全局 */ - this.GroupCD[e.group_id] = true - setTimeout(() => delete this.GroupCD[e.group_id], config.GroupCD * 1000) - - /** 个人 */ - this.GroupUserCD[key] = true - setTimeout(() => delete this.GroupUserCD[key], config.GroupUserCD * 1000) - return true - } - return true - } - - /** 启用单个群聊所有消息冷却时间 */ - if (this.App.GroupConfig.GroupCD) { - this.CD = (e, config) => { - /** cd中... */ - if (this.GroupCD[e.group_id]) return false - /** 全局 */ - this.GroupCD[e.group_id] = true - setTimeout(() => delete this.GroupCD[e.group_id], config.GroupCD * 1000) - return true - } - return true - } - - /** 启用单个群聊个人消息冷却时间 */ - if (this.App.GroupConfig.GroupUserCD) { - this.CD = (e, config) => { - const key = `${e.group_id}.${e.user_id}` - /** cd中... */ - if (this.GroupUserCD[key]) return false - /** 个人 */ - this.GroupUserCD[key] = true - setTimeout(() => delete this.GroupUserCD[key], config.GroupUserCD * 1000) - return true - } - return true - } - - /** 都没有启用 */ - this.CD = () => true - } - - /** - * 响应模式 - */ - #mode () { - /** 启用 */ - if (this.App.GroupConfig.mode) { - this.mode = (e, config) => { - const modeMap = { - 0: () => true, - 1: () => e.atBot, - 2: () => e.isMaster, - 3: () => !!e.alias, - 4: () => { - if (e.atBot) return true - return !!e.alias - }, - 5: () => { - if (e.isMaster) return true - if (e.atBot) return true - return !!e.alias - }, - } - return modeMap[Number(config.mode) || 0]() - } - return true - } - - /** 未启用 */ - this.mode = () => true - } - - /** - * 前缀、别名 - */ - #alias () { - /** 启用 */ - if (this.App.GroupConfig.alias) { - this.alias = (e, config) => { - const aliasRegex = new RegExp(`^(${config.alias.join('|')})`) - const match = e.msg.match(aliasRegex) - if (match) { - e.msg = e.msg.replace(aliasRegex, '').trim() - e.alias = match[1] - } - } - return true - } - - /** 未启用 */ - this.alias = () => true - } -} - -export default new Review() diff --git a/lib/index.js b/lib/index.js index d9f7b98..a1b1150 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,132 +1 @@ -import Redis from './db/redis.js' -import LevelDB from './db/level.js' -import Logger from './core/logger.js' -import Config from './core/config.js' -import Server from './core/server.js' -import { exec } from './utils/exec.js' -import Common from './common/common.js' -import Process from './core/process.js' -import Listener from './core/listener.js' -import Puppeteer from './renderer/Server.js' -import Kritor from './adapter/kritor/index.js' -import RenderClient from './renderer/Client.js' -import onebot11, { OneBot11 } from './adapter/onebot/OneBot11.js' -import App from './utils/app.js' -import Button from './utils/button.js' -import Ffmpeg from './utils/ffmpeg.js' -import Handler from './utils/handler.js' -import { KarinMessage } from './bot/KarinMessage.js' -import { KarinAdapter as adapter } from './adapter/adapter.js' -import Update from './utils/update.js' -import YamlEditor from './utils/YamlEditor.js' -import { kritor } from 'kritor-proto' -import render from './renderer/App.js' -import RenderBase from './renderer/Base.js' -import * as protobuf from './utils/protobuf.js' -import plugin from './plugins/plugin.js' -import Segment from './utils/segment.js' -import Message from './event/message.js' -import Notice from './event/notice.js' -import Request from './event/request.js' -import Online from './core/online.js' - -export const config = new Config() -export const segment = new Segment() -export const button = new Button() -export const handler = new Handler() -export const update = new Update() - -/** - * @type {boolean} 是否为开发者模式 - */ -process.env.KarinDev = !!process.argv[2]?.includes('dev') -const isDev = process.env.KarinMode === 'dev' ? '[开发模式]' : '[生产模式]' - -/** - * 日志模块初始化 - */ -export const logger = new Logger().config(config).logger() -export const common = new Common(logger, segment) - -/** - * 全局变量 - */ -global.logger = logger -logger.debug(`[初始化]${isDev} 日志模块初始化完成 (1/10)`) - -/** - * 监听器初始化 - */ -export const listener = new Listener(logger, common, config, segment) -export const Bot = listener -logger.debug(`[初始化]${isDev} 监听器初始化完成 (2/10)`) - -/** - * 进程初始化 - */ -const Proces = await new Process(logger, common, config, listener).process().check() -logger.debug(`[初始化]${isDev} 进程初始化完成 (3/10)`) - -/** - * 适配器初始化 - */ -listener.emit('adapter', onebot11) -listener.emit('adapter', Puppeteer) -listener.emit('adapter', { type: 'grpc', adapter: undefined, path: 'http://' + config.Server.grpc.host }) -logger.debug(`[初始化]${isDev} 适配器初始化完成 (4/10)`) - -/** - * 服务器初始化 - */ -new Kritor(logger, common, config, listener).init() -new Server(logger, common, config, listener, exec).init() -logger.debug(`[初始化]${isDev} 服务器初始化完成 (5/10)`) - -/** - * 主动适配器初始化 - */ -const Onebot11 = config.Server.websocket.OneBot11Host -if (Array.isArray(Onebot11) && Onebot11.length) { - for (const url of Onebot11) { - new OneBot11(logger, common, listener, config).client(url) - } -} -const renderCfg = config.Server.websocket.render -if (Array.isArray(renderCfg) && renderCfg.length) { - for (const url of renderCfg) { - new RenderClient(url).start() - } -} -logger.debug(`[初始化]${isDev} 主动适配器初始化完成 (6/10)`) - -/** - * 数据库驱动器初始化 - */ -export const level = new LevelDB() -export const redis = await new Redis(logger, config).start() -// eslint-disable-next-line no-unused-vars -let online = new Online(level, segment, listener) -setTimeout(() => { online = null }, 60000) -logger.debug(`[初始化]${isDev} 数据库驱动器初始化完成 (7/10)`) - -/** - * 辅助工具初始化 - */ - -export const ffmpeg = await Ffmpeg(logger, config) -export { kritor, plugin, protobuf, YamlEditor, RenderBase, exec, App, KarinMessage, adapter, render, render as Renderer, update as Update, config as Cfg, plugin as Plugin, Proces as Process } -logger.debug(`[初始化]${isDev} 辅助工具初始化完成 (8/10)`) - -/** - * 插件初始化 - */ -listener.emit('plugin') -logger.debug(`[初始化]${isDev} 插件初始化完成 (9/10)`) - -/** - * 初始化完毕开始监听事件 - */ -listener.on('message', data => new Message(data)) -listener.on('notice', data => new Notice(data)) -listener.on('request', data => new Request(data)) -logger.debug(`[初始化]${isDev} 事件监听初始化完成 (10/10)`) +export * from 'node-karin' diff --git a/lib/plugins/loader.js b/lib/plugins/loader.js deleted file mode 100644 index bfdc0a6..0000000 --- a/lib/plugins/loader.js +++ /dev/null @@ -1,409 +0,0 @@ -import fs from 'fs' -import lodash from 'lodash' -import path from 'path' -import util from 'util' -import chokidar from 'chokidar' -import schedule from 'node-schedule' -import Renderer from '../renderer/App.js' -import { logger, handler, button, common, listener } from '../index.js' - -/** - * 加载插件 - */ -class Loader { - constructor () { - /** - * 插件列表 - * @type {{ - * App: import('./plugin.js').default, - * file: { - * name: string, - * dir: string - * }, - * name: string, - * event: string, - * priority: number, - * permission: string, - * accept: boolean, - * rule: Array<{reg: RegExp, fnc: string, event?: string, permission?: string, log?: Function}>, - * }[]} - */ - this.Apps = [] - /** - * 定时任务 - * @type {Array<{ - * App: import('./plugin.js').default, - * file: { name: string, dir: string }, - * cron: string, - * fnc: string, - * log: Function, - * name: string - * }>} - */ - this.task = [] - this.dir = './plugins' - - /** 插件监听 */ - this.watcher = {} - /** 热更新收集 */ - this.watchList = [] - } - - /** - * 插件初始化 - * @returns {Promise} - */ - async load () { - const files = this.getPlugins() - - listener.once('plugin.loader', () => { - for (const v of this.watchList) { - v.type === 'folder' ? this.watchDir(v.dir) : this.watch(v.dir, v.name) - } - }) - - logger.info(logger.green('-----------')) - logger.info('加载插件中..') - - /** 载入插件 */ - const promises = files.map(async ({ dir, name }) => await this.createdApp(dir, name, false)) - - /** 等待所有插件加载完成 */ - await Promise.all(promises) - - /** 创建定时任务 */ - this.creatTask() - - const handlerKeys = Object.keys(handler.events) - let handlerCount = 0 - handlerKeys.forEach(key => { - handlerCount += handler.events[key].length - }) - - logger.info(`[按钮][${button.Apps.length}个] 加载完成`) - logger.info(`[插件][${this.Apps.length}个] 加载完成`) - logger.info(`[渲染器][${Renderer.Apps.length}个] 加载完成`) - logger.info(`[定时任务][${this.task.length}个] 加载完成`) - logger.info(`[Handler][Key:${handlerKeys.length}个][fnc:${handlerCount}个] 加载完成`) - logger.info(logger.green('-----------')) - logger.info(`Karin启动完成:耗时 ${logger.green(process.uptime().toFixed(2))} 秒...`) - /** 优先级排序 */ - this.orderBy() - listener.emit('plugin.loader') - return this - } - - /** - * 获取所有插件 - * @returns {Array<{dir: string, name: string}>} 插件信息 - */ - getPlugins () { - const App = [] - /** 获取所有插件包 */ - const plugins = common.getPlugins() - - for (const val of plugins) { - const dir = val.name - const _path = `${this.dir}/${dir}` - /** 带有package.json的视为插件包 */ - if (fs.existsSync(`${_path}/package.json`)) { - /** index.js存在 */ - if (fs.existsSync(`${_path}/index.js`)) { - App.push({ dir, name: 'index.js' }) - /** dev监听index.js */ - if (process.argv[2]?.includes('dev')) this.hotUpdate('file', dir, 'index.js') - } - /** apps存在 */ - if (fs.existsSync(`${_path}/apps`) && fs.statSync(`${_path}/apps`).isDirectory()) { - App.push(...this.getApps(`${dir}/apps`)) - } - } else { - App.push(...this.getApps(dir)) - } - } - - return App - } - - /** - * 获取app信息 - * @param {string} dir 文件夹目录 - * @returns {Array<{dir: string, name: string}>} 应用信息 - */ - getApps (dir) { - const App = [] - const apps = fs.readdirSync(`${this.dir}/${dir}`, { withFileTypes: true }) - for (const app of apps) { - /** 忽略非js文件 */ - if (!app.name.endsWith('.js')) continue - /** 收集插件 */ - App.push({ dir, name: app.name }) - } - if (dir.endsWith('apps')) { - /** dev监听apps热更 */ - if (process.argv[2]?.includes('dev')) this.hotUpdate('folder', dir) - } else { - /** 监听文件夹热更新 */ - this.hotUpdate('folder', dir) - } - return App - } - - /** - * 构建插件缓存对象 - * @param {import('./plugin.js').default} App - * @param {import('./plugin.js').default} Class new App() - */ - App (App, Class, file) { - const app = { - App, - /** 插件文件信息 */ - file, - name: Class.name, - event: Class.event || 'message', - priority: Class.priority ?? 5000, - permission: Class.permission || 'all', - accept: !!Class.accept, - rule: Class.rule || [], - } - - /** 进一步处理rule */ - app.rule.forEach((val, index) => { - app.rule[index].reg = new RegExp(val.reg) - app.rule[index].log = val.log === false ? (id, log) => logger.debug('mark', id, log) : (id, log) => logger.bot('mark', id, log) - }) - - return app - } - - /** 排序 */ - orderBy () { - this.Apps = lodash.orderBy(this.Apps, ['priority'], ['asc']) - button.Apps = lodash.orderBy(button.Apps, ['priority'], ['asc']) - } - - /** 新增插件 */ - async createdApp (dir, name, isOrderBy = false) { - try { - let path = `../../plugins/${dir}/${name}` - if (isOrderBy) path = path + `?${Date.now()}` - const tmp = await import(path) - lodash.forEach(tmp, (App) => { - if (!App.prototype) return - /** new App() */ - const Class = new App() - logger.debug(`载入插件 [${name}][${Class.name}]`) - /** 执行初始化 */ - Class.init && Class.init() - - /** 收集定时任务 */ - lodash.forEach(Class.task, (val) => { - if (!val.name) return logger.error(`[${dir}][${name}] 定时任务name错误`) - if (!val.cron) return logger.error(`[${dir}][${name}] 定时任务cron错误:${Class.name}`) - this.task.push({ App, file: { dir, name }, cron: val.cron, fnc: val.fnc, name: val.name, log: val.log ?? true }) - }) - - /** 注册Handler */ - if (!lodash.isEmpty(Class.handler)) handler.add({ name, dir, App, Class }) - - /** 注册按钮 */ - if (!lodash.isEmpty(Class.button)) button.add({ name, dir, App, Class }) - - /** 收集插件 */ - this.Apps.push(this.App(App, Class, { name, dir })) - return true - }) - - if (isOrderBy) { - /** 创建定时任务 */ - this.creatTask() - /** 优先级排序 */ - this.orderBy() - } - return true - } catch (error) { - if (/Cannot find package '(.+?)'/.exec(error)?.[1]) { - logger.debug(error.stack || error.message || JSON.stringify(error)) - const pack = /Cannot find package '(.+?)'/.exec(error)?.[1] - logger.error(logger.red('--------插件载入错误--------')) - logger.mark(`错误: [${dir}][${name}] 缺少必要的依赖项: ${logger.red(pack)}`) - logger.mark(`操作:请尝试在命令终端中执行 ${logger.red('pnpm i -P')} 命令安装依赖项`) - logger.mark('提示:如安装后仍未解决,可选择以下方案') - logger.mark(` 1.手工安装依赖: ${logger.red('pnpm i ' + pack)}`) - logger.mark(` 2.联系插件作者:联系插件作者将 ${logger.red(pack)} 依赖项添加至插件的package.json文件中的dependencies字段中`) - logger.error(logger.red('-----------------------------')) - } else { - logger.error(`载入插件错误:${logger.red(`${dir}/${name}`)}`) - logger.error(error) - } - return false - } - } - - /** 卸载插件 */ - uninstallApp (dir, name) { - this.Apps = this.Apps.filter(v => !(v.file.dir === dir && v.file.name === name)) - this.uninstallTask(dir, name) - button.del(dir, name) - handler.del({ dir, name }) - } - - /** 创建定时任务 */ - async creatTask () { - this.task.forEach((val, index) => { - if (val.schedule) return - val.log = val.log === false ? () => '' : (log) => logger.mark(log) - val.schedule = schedule.scheduleJob(val.cron, async () => { - try { - val.log(`[定时任务][${val.file.dir}][${val.name}] 开始执行`) - const App = new val.App() - let res = App[val.fnc] && App[val.fnc]() - if (util.types.isPromise(res)) res = await res - val.log(`[定时任务][${val.file.dir}][${val.name}] 执行完毕`) - } catch (error) { - logger.error(`[定时任务][${val.file.dir}][${val.name}] 执行报错`) - logger.error(error) - } - }) - this.task[index] = val - }) - } - - /** 卸载定时任务 */ - uninstallTask (dir, name) { - this.task = this.task.filter(task => { - if (task.file.dir === dir && task.file.name === name) { - /** 停止定时任务 */ - task.schedule.cancel() - /** 移除定时任务 */ - return false - } - /** 保留定时任务 */ - return true - }) - } - - /** - * 热更新存储 - * @param {'folder'|'file'} type 类型 - * @param {string} dir 文件夹名称 - * @param {string} [name] 文件名称 只有在type为folder时才需要 - */ - async hotUpdate (type, dir, name) { - type === 'folder' ? this.watchList.push({ type, dir }) : this.watchList.push({ type, dir, name }) - } - - /** - * 监听单个文件热更新 - * @param {string} dir 文件夹名称 - * @param {string} name 文件名称 - */ - watch (dir, name) { - if (this.watcher[`${dir}.${name}`]) return - - const file = `./plugins/${dir}/${name}` - const watcher = chokidar.watch(file) - - /** 监听修改 */ - watcher.on('change', async () => { - /** 卸载 */ - this.uninstallApp(dir, name) - /** 载入插件 */ - const res = await this.createdApp(dir, name, true) - if (!res) return - logger.mark(`[修改插件][${dir}][${name}]`) - }) - - /** 监听删除 */ - watcher.on('unlink', async () => { - /** 卸载 */ - this.uninstallApp(dir, name) - delete this.watcher[`${dir}.${name}`] - logger.mark(`[卸载插件][${dir}][${name}]`) - }) - - this.watcher[`${dir}.${name}`] = watcher - } - - /** - * 监听文件夹更新 - * @param {string} dir 文件夹名称 - */ - watchDir (dir) { - if (this.watcher[dir]) return - - const file = `${this.dir}/${dir}/` - const watcher = chokidar.watch(file) - - /** 热更新 */ - setTimeout(() => { - /** 新增文件 */ - watcher.on('add', async filePath => { - logger.debug(`[热更新][新增插件] ${filePath}`) - const name = path.basename(filePath) - if (!name.endsWith('.js')) return - if (!fs.existsSync(`${file}/${name}`)) return - - /** 载入插件 */ - const res = await this.createdApp(dir, name, true) - if (!res) return - /** 延迟1秒 等待卸载完成 */ - - common.sleep(1000) - .then(() => { - /** 停止整个文件夹监听 */ - watcher.close() - /** 新增插件之后重新监听文件夹 */ - delete this.watcher[dir] - this.watchDir(dir) - logger.mark(`[新增插件][${dir}][${name}]`) - return true - }) - .catch((error) => logger.error(error)) - }) - - /** 监听修改 */ - watcher.on('change', async PluPath => { - const name = path.basename(PluPath) - if (!name.endsWith('.js')) return - if (!fs.existsSync(`${this.dir}/${dir}/${name}`)) return - - /** 卸载 */ - this.uninstallApp(dir, name) - /** 载入插件 */ - const res = await this.createdApp(dir, name, true) - if (!res) return - - logger.mark(`[修改插件][${dir}][${name}]`) - }) - - /** 监听删除 */ - watcher.on('unlink', async PluPath => { - const name = path.basename(PluPath) - if (!name.endsWith('.js')) return - - /** 卸载 */ - this.uninstallApp(dir, name) - /** 停止监听 */ - watcher.close() - /** 重新监听文件夹 */ - delete this.watcher[dir] - this.watchDir(dir) - logger.mark(`[卸载插件][${dir}][${name}]`) - }) - }, 500) - - /** 生成随机数0.5-2秒 */ - const random = Math.floor(Math.random() * 1000) + 500 - common.sleep(random) - .then(() => { - /** 这里需要检查一下是否已经存在,已经存在就关掉之前的监听 */ - if (this.watcher[dir]) this.watcher[dir].close() - this.watcher[dir] = watcher - return true - }) - .catch(() => { }) - } -} - -export default new Loader() diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js deleted file mode 100644 index b143271..0000000 --- a/lib/plugins/plugin.js +++ /dev/null @@ -1,190 +0,0 @@ -/** - * @typedef {string | import('../bot/KarinElement.js').KarinElement |Array} element - 发送的消息结构 - */ - -/** - * 上下文状态 - * @type {object} - * @property {plugin} plugin 插件 - * @property {string} fnc 执行方法 - */ -const stateArr = {} -export { stateArr } - -/** - * 插件基类 - * @type {KarinPlugin} - */ -export default class plugin { - /** - * event - * @type {import('../bot/KarinEvent.js').KarinEvent | import('../bot/KarinNotice.js').KarinNotice | import('../bot/KarinMessage.js').KarinMessage | import('../bot/KarinRequest.js').KarinRequest} - */ - e - - /** - * @param {KarinPlugin} - */ - constructor ({ - name, - dsc, - event = 'message', - priority = 5000, - task = [], - rule = [], - handler = [], - button = [], - }) { - /** - * @type {string} 插件名称 - */ - this.name = name - /** - * @type {string} 插件描述 - */ - this.dsc = dsc - /** - * @type {string} 监听事件 - */ - this.event = event - /** - * @type {number} 优先级 - */ - this.priority = priority - /** - * @type {Array<{name: string, cron: string, fnc: string, log?: boolean}>} 定时任务 - */ - this.task = task - /** - * @type {Array<{reg: string, event?: string, fnc: string, permission?: 'master'|'admin'|'group.owner'|'group.admin'|'all'}>} 命令规则 - */ - this.rule = rule - /** - * @type {Array<{key: string, fnc: string, priority: number}>} 按钮 - */ - this.button = button - /** - * @type {Array<{key: string, fnc: string, priority: number}>} handler - */ - this.handler = handler - } - - /** - * 快速回复 - * @param {element} element - 发送的消息 - * @param {object} options - 回复数据 - * @param {boolean?} options.at - 是否at用户 - * @param {boolean?} options.reply - 是否引用回复 - * @param {number?} options.recallMsg - 群聊是否撤回消息,0-120秒,0不撤回 - * @param {boolean?} options.button - 是否使用按钮 - * @param {number?} options.retry_count - 重试次数 - * @returns {Promise<{ message_id?: string }>} - 返回消息ID - */ - reply (element = '', options = { reply: false, recallMsg: 0, at: false, button: false, retry_count: 1 }) { - return this.e.reply(element, options) - } - - /** - * 快速回复合并转发 - * @param {Array} element - * @return {Promise<{message_id}>} - */ - async replyForward (element) { - await this.e.bot.sendForwardMessage(this.e.contact, element) - } - - /** - * 构建上下文键 - * @returns {string} - 上下文键 - */ - conKey () { - return `${this.e.isGroup ? `${this.e.group_id}.` : ''}` + (this.userId || this.e.user_id) - } - - /** - * 设置上下文状态 - * @param {string | Function} fnc - 执行方法 - * @param {boolean} reply - 超时后是否回复 - * @param {number} time - 操作时间,默认120秒 - */ - setContext (fnc, reply = true, time = 120) { - const key = this.conKey() - stateArr[key] = { plugin: this, fnc } - /** 操作时间 */ - this.timeout = setTimeout(() => { - if (stateArr[key]) { - delete stateArr[key] - if (reply) this.e.reply('操作超时已取消', { at: true }) - } - }, time * 1000) - } - - /** - * 获取上下文状态 - * @returns {StateArr} - 上下文状态对象 - */ - getContext () { - const key = this.conKey() - return stateArr[key] - } - - /** - * 清除上下文状态 - */ - finish () { - const key = this.conKey() - if (stateArr[key] && stateArr[key]) { - /** 清除定时器 */ - clearTimeout(this.timeout) - delete stateArr[key] - } - } -} - -/** - * @typedef {object} StateArr 上下文状态对象 - * @property {plugin} StateArr.plugin 插件 - * @property {string} StateArr.fnc 执行方法 - */ - -/** - * @typedef {object} KarinPlugin - * @property {string} KarinPlugin.name 插件名称 - * @property {string} KarinPlugin.dsc 插件描述 - * @property {string} KarinPlugin.event 监听事件 - * @property {number} KarinPlugin.priority 优先级 - * @property {Array} KarinPlugin.task 定时任务 - * @property {Array} KarinPlugin.rule 命令规则 - * @property {Array} KarinPlugin.button 按钮 - * @property {Array} KarinPlugin.handler handler - */ - -/** - * @typedef {object} KarinTask - * @property {string} KarinTask.name 定时任务名称 - * @property {string} KarinTask.cron 定时任务cron表达式 - * @property {string} KarinTask.fnc 定时任务方法名 - * @property {boolean} [KarinTask.log] 是否显示执行日志 - */ - -/** - * @typedef {object} KarinRule - * @property {string} KarinRule.reg 命令正则 - * @property {string} KarinRule.fnc 命令执行方法 - * @property {string} [KarinRule.event] 子事件 - * @property {boolean} [KarinRule.log] 是否显示执行日志 - * @property {'master'|'admin'|'group.owner'|'group.admin'|'all'} [KarinRule.permission] 子权限 - */ - -/** - * @typedef {object} KarinButton - * @property {string} KarinButton.reg 按钮命令正则 - * @property {string} KarinButton.fnc 按钮执行方法 - */ - -/** - * @typedef {object} KarinHandler - * @property {string} KarinHandler.key handler支持的事件key - * @property {string} KarinHandler.fnc handler的处理fnc - * @property {number} [KarinHandler.priority] handler优先级 - */ diff --git a/lib/renderer/App.js b/lib/renderer/App.js deleted file mode 100644 index ffd6019..0000000 --- a/lib/renderer/App.js +++ /dev/null @@ -1,126 +0,0 @@ -// eslint-disable-next-line no-unused-vars -import { logger, RenderBase } from '#Karin' - -/** - * 渲染器管理器 - */ -class Renderer { - constructor () { - /** 索引 */ - this.index = 0 - /** - * 渲染器列表 - * @type {APP[]} - */ - this.Apps = [] - } - - /** - * 注册渲染器 - * @param {object} data 渲染器数据 - * @param {string} data.id 渲染器ID - * @param {'image'|string} data.type 渲染器类型 - * @param {RenderBase.render} data.render 渲染器标准方法 - * @returns {number} 渲染器索引 - */ - app (data) { - this.index++ - const index = this.index - const { id, type = 'image', render } = data - if (!id) throw new Error('[注册渲染器失败] 缺少渲染器ID') - if (!type) throw new Error('[注册渲染器失败] 缺少渲染器类型') - if (!render) throw new Error('[注册渲染器失败] 缺少渲染器标准方法') - - const time = Date.now() - const options = { index, id, type, render, time } - this.Apps.push(options) - logger.mark(`${logger.violet(`[渲染器:${index}]`)} 注册成功: ` + logger.green(id)) - return index - } - - /** - * 卸载渲染器 - * @param {number} index 渲染器索引 - * @returns {boolean} 是否卸载成功 - */ - unapp (index) { - const app = this.Apps.find(app => app.index === index) - if (!app) { - logger.error(`[卸载渲染器失败] 未找到渲染器索引:${index}`) - return false - } - - this.Apps = this.Apps.filter(app => app.index !== index) - logger.mark(`[卸载渲染器] ${app.id}`) - return true - } - - /** - * 返回渲染器实例 未键入id返回第一个 - * @param {string} id 渲染器ID - * @returns {Promise} 渲染器实例 - */ - App (id) { - if (this.Apps.length === 0) throw new Error('[调用渲染器失败] 渲染器列表为空') - if (!id) return this.Apps[0] - - /** 筛选出id一致的渲染器 */ - const app = this.Apps.find(app => app.id === id) - if (!app) throw new Error(`[调用渲染器失败] 未找到渲染器:${id}`) - return app - } - - /** - * 调用标准渲染器 - * @param {object} options 渲染参数 - * @param {string} options.file http地址或本地文件路径 - * @param {string} [options.name] 模板名称 - * @param {string} [options.fileID] art-template后的文件名 - * @param {object} [options.data] 传递给模板的数据 template.render(data) - * @param {'png'|'jpeg'|'webp'} [options.type] 截图类型 默认'webp' - * @param {number} [options.quality] 截图质量 默认90 1-100 - * @param {boolean} options.omitBackground 是否隐藏背景 默认false - * @param {object} [options.setViewport] 设置视窗大小和设备像素比 默认1920*1080、1 - * @param {number} [options.setViewport.width] 视窗宽度 - * @param {number} [options.setViewport.height] 视窗高度 - * @param {string} [options.setViewport.deviceScaleFactor] 设备像素比 - * @param {number|boolean} [options.multiPage] 分页截图 传递数字则视为视窗高度 返回数组 - * @param {object} [options.pageGotoParams] 页面goto时的参数 - * @param {number} [options.pageGotoParams.timeout] 页面加载超时时间 - * @param {'load'|'domcontentloaded'|'networkidle0'|'networkidle2'} [options.pageGotoParams.waitUntil] 页面加载状态 - * @param {string} [id] 渲染器ID 1 - * @returns {Promise} 返回图片base64或数组 - */ - async render (options, id) { - const res = await this.App(id) - return res.render(options) - } - - /** - * 快速渲染 - * @param {string} data html路径、http地址 - * @returns {Promise} 返回图片base64或数组 - */ - async renderHtml (data) { - const app = this.App() - const options = { - file: data, - name: 'render', - pageGotoParams: { - waitUntil: 'networkidle2' - } - } - return app.render(options) - } -} - -export default new Renderer() - -/** - * @typedef {object} APP 渲染器实例 - * @property {number} index 渲染器索引 - * @property {string} id 渲染器ID - * @property {string} type 渲染器类型 - * @property {number} time 注册时间 - * @property {RenderBase.render} render 渲染器标准方法 - */ diff --git a/lib/renderer/Base.js b/lib/renderer/Base.js deleted file mode 100644 index e5bae16..0000000 --- a/lib/renderer/Base.js +++ /dev/null @@ -1,99 +0,0 @@ -import fs from 'fs' -import { common } from '#Karin' -import chokidar from 'chokidar' -import template from 'art-template' - -/** - * 渲染器基类 所有渲染器都应该继承这个类 - */ -export default class RenderBase { - /** - * @type {RenderFunction} - */ - RenderFunction - constructor () { - this.dir = './temp/html' - this.html = {} - this.watcher = {} - common.mkdir(this.dir) - } - - /** - * 模板渲染 - * @param {options} options 模板名称 - * @param {boolean} isAbs 是否返回绝对路径 - * @return {string} 渲染完成的文件路径 - */ - dealTpl (options, isAbs = true) { - let { name, fileID, file: tplFile } = options - fileID = fileID || name - const filePath = `./temp/html/${name}/${fileID}.html` - - /** 读取html模板 */ - if (!this.html[tplFile]) { - common.mkdir(`./temp/html/${name}`) - - try { - this.html[tplFile] = fs.readFileSync(tplFile, 'utf8') - } catch (error) { - logger.error(`加载html错误:${tplFile}`) - return false - } - - this.watch(tplFile) - } - - /** 替换模板 */ - const tmpHtml = template.render(this.html[tplFile], options.data) - - /** 保存模板 */ - fs.writeFileSync(filePath, tmpHtml) - - logger.debug(`[图片生成][使用模板] ${filePath}`) - - /** 是否返回绝对路径 */ - if (isAbs) return `${process.cwd()}/temp/html/${name}/${fileID}.html` - - return filePath - } - - /** - * 监听模板文件 - * @param {string} tplFile 模板文件路径 - */ - watch (tplFile) { - if (this.watcher[tplFile]) return - - const watcher = chokidar.watch(tplFile) - watcher.on('change', () => { - delete this.html[tplFile] - logger.mark(`[修改html模板] ${tplFile}`) - }) - - this.watcher[tplFile] = watcher - } -} - -/** - * @typedef {function(options): Promise} RenderFunction - * @returns {string|string[]} 返回图片base64或数组 - */ - -/** - * @typedef {object} options 渲染参数 - * @property {string} options.file http地址或本地文件路径 - * @property {string} [options.name] 模板名称 - * @property {string} [options.fileID] art-template后的文件名 - * @property {object} [options.data] 传递给模板的数据 template.render(data) - * @property {'png'|'jpeg'|'webp'} [options.type] 截图类型 默认'webp' - * @property {number} [options.quality] 截图质量 默认90 1-100 - * @property {boolean} options.omitBackground 是否隐藏背景 默认false - * @property {object} [options.setViewport] 设置视窗大小和设备像素比 默认1920*1080、1 - * @property {number} [options.setViewport.width] 视窗宽度 - * @property {number} [options.setViewport.height] 视窗高度 - * @property {string} [options.setViewport.deviceScaleFactor] 设备像素比 - * @property {number|boolean} [options.multiPage] 分页截图 传递数字则视为视窗高度 返回数组 - * @property {object} [options.pageGotoParams] 页面goto时的参数 - * @property {number} [options.pageGotoParams.timeout] 页面加载超时时间 - * @property {'load'|'domcontentloaded'|'networkidle0'|'networkidle2'} [options.pageGotoParams.waitUntil] 页面加载状态 - */ diff --git a/lib/renderer/Client.js b/lib/renderer/Client.js deleted file mode 100644 index 31dc334..0000000 --- a/lib/renderer/Client.js +++ /dev/null @@ -1,172 +0,0 @@ -import fs from 'fs' -import WebSocket from 'ws' -import { randomUUID } from 'crypto' -import { logger, Renderer, Bot, common, RenderBase } from '#Karin' - -export default class RenderClient extends RenderBase { - constructor (url) { - super() - this.url = url - this.type = 'image' - this.id = 'puppeteer' - this.index = 0 - this.retry = 0 - this.reg = new RegExp(`(${process.cwd().replace(/\\/g, '\\\\')}|${process.cwd().replace(/\\/g, '/')})`, 'g') - } - - /** - * 初始化 - */ - async start () { - /** 连接ws */ - this.ws = new WebSocket(this.url) - /** 建立连接 */ - this.ws.on('open', () => { - logger.mark(`[渲染器:${this.id}][WebSocket] 建立连接:${logger.green(this.url)}`) - /** 注册渲染器 */ - try { - this.index = Renderer.app({ id: this.id, type: this.type, render: this.render.bind(this) }) - this.retry = 0 - } catch (error) { - logger.error(`[渲染器:${this.id}] 注册渲染器失败:`, error) - /** 断开连接 */ - this.ws.close() - } - /** 心跳 */ - this.heartbeat() - /** 监听消息 */ - this.ws.on('message', (data) => this.message(data)) - }) - - /** 监听断开 */ - this.ws.once('close', async () => { - this.retry++ - /** 停止监听 */ - this.ws.removeAllListeners() - /** 卸载渲染器 */ - this.index && Renderer.unapp(this.index) && (this.index = 0) - logger.warn(`[渲染器:${this.id}][重连次数:${this.retry}] 连接断开,5秒后将尝试重连:${this.url}`) - await common.sleep(5000) - await this.start() - }) - - /** 监听错误 */ - this.ws.on('error', async (e) => { - logger.debug(e) - await common.sleep(5000) - this.ws.close() - }) - } - - /** - * 心跳 - */ - async heartbeat () { - /** 无限循环 错误则停止 */ - while (true) { - try { - this.ws.send(JSON.stringify({ action: 'heartbeat' })) - logger.debug(`[渲染器:${this.id}] 心跳:${this.url}`) - } catch (e) { - logger.debug(`[渲染器:${this.id}] 心跳失败:`, e) - this.ws.close() - break - } - await common.sleep(5000) - } - } - - /** - * 接受消息 - */ - async message (data) { - data = JSON.parse(data) - switch (data.action) { - /** 静态文件 */ - case 'static': { - let file = decodeURIComponent(data.params.file) - logger.debug(`[渲染器:${this.id}][正向WS] 访问静态文件:${file}`) - file = fs.readFileSync('.' + file) - const params = { - echo: data.echo, - action: 'static', - status: 'ok', - data: { file } - } - return this.ws.send(JSON.stringify(params)) - } - /** 渲染结果 */ - case 'renderRes': { - Bot.emit(data.echo, data) - break - } - /** 未知数据 */ - default: { - logger.warn(`[渲染器:${this.id}] 收到未知数据:`, data) - } - } - } - - /** - * 渲染模板 - * @param {object} options 渲染参数 - * @param {string} options.file http地址或本地文件路径 - * @param {string} [options.name] 模板名称 - * @param {string} [options.fileID] art-template后的文件名 - * @param {object} [options.data] 传递给模板的数据 template.render(data) - * @param {'png'|'jpeg'|'webp'} [options.type] 截图类型 默认'webp' - * @param {number} [options.quality] 截图质量 默认90 1-100 - * @param {boolean} options.omitBackground 是否隐藏背景 默认false - * @param {object} [options.setViewport] 设置视窗大小和设备像素比 默认1920*1080、1 - * @param {number} [options.setViewport.width] 视窗宽度 - * @param {number} [options.setViewport.height] 视窗高度 - * @param {string} [options.setViewport.deviceScaleFactor] 设备像素比 - * @param {number|boolean} [options.multiPage] 分页截图 传递数字则视为视窗高度 返回数组 - * @param {object} [options.pageGotoParams] 页面goto时的参数 - * @param {number} [options.pageGotoParams.timeout] 页面加载超时时间 - * @param {'load'|'domcontentloaded'|'networkidle0'|'networkidle2'} [options.pageGotoParams.waitUntil] 页面加载状态 - * @returns {Promise} 返回图片base64或数组 - */ - async render (options) { - /** 渲染模板 */ - let file = options.file - let action = 'renderHtml' - if (options.file.includes('http') || options.vue) { - action = 'render' - } else { - file = this.dealTpl(options) - /** 判断是本地karin-puppeteer还是远程 */ - if (!/127\.0\.0\.1|localhost/.test(this.url)) { - file = fs.readFileSync(file, 'utf-8').replace(this.reg, '') - } else { - action = 'render' - file = 'file://' + file - } - } - - if (!file) { - logger.error(`[渲染器:${this.id}:${this.index}] 渲染文件不存在:${options.file}`) - return false - } - - /** 编码 */ - file = encodeURIComponent(file) - - const data = options - const echo = randomUUID() - /** 移除掉模板参数 */ - if (data.data) delete data.data - data.file = file - - const req = JSON.stringify({ echo, action, data }) - logger.debug(`[渲染器:${this.id}:${this.index}][正向WS] 请求:${this.url} \nhtml: ${options.file} \ndata: ${JSON.stringify(data)}`) - this.ws.send(req) - - return new Promise((resolve, reject) => { - Bot.once(echo, data => { - if (data.ok) return resolve(data.data) - reject(new Error(JSON.stringify(data))) - }) - }) - } -} diff --git a/lib/renderer/Http.js b/lib/renderer/Http.js deleted file mode 100644 index 05002cc..0000000 --- a/lib/renderer/Http.js +++ /dev/null @@ -1,57 +0,0 @@ -import axios from 'axios' -import RenderBase from './Base.js' - -export default class HttpRenderer extends RenderBase { - /** - * 构造函数 - * @param {string} host - 静态服务器地址 - * @param {string} url - 渲染接口 - * @param {string} token - token - */ - constructor (host, url, token) { - super() - this.id = 'puppeteer' - this.host = host - this.url = url - this.token = token - } - - /** - * 渲染 - * @param {RenderBase.RenderFunction} options - 渲染参数 - * @returns {Promise} 渲染结果 - */ - async render (options) { - const name = options.name || 'render' - let file = options.file - /** 非http渲染模板并转为http静态资源 */ - if (!options.file.includes('http') && !options.vue) { - const isLocalhost = this.host.includes('127.0.0.1') || this.host.includes('localhost') - file = this.dealTpl(options, isLocalhost) - if (!file) { - logger.error(`[渲染器:${this.id}] 模板文件不存在:${name}`) - return false - } - - options.file = isLocalhost ? 'file://' + file : `${this.host}/api/renderHtml?html=${file}` - } - - delete options.data - const data = options - const headers = { - Authorization: this.token - } - logger.debug(`[渲染器:${this.id}][POST] \n请求:${this.url} \nhtml: ${options.file} \ndata: ${JSON.stringify(data)}`) - const res = await axios({ method: 'post', url: this.url, headers, data }) - if (res.status === 200 && res.data.ok) { - return res.data.data - } - throw new Error(`渲染失败:${JSON.stringify(res.data)}`) - } -} - -/** - * @typedef {function(RenderBase.render): Promise} RenderFunction - * @param {RenderBase.render} options 渲染参数 - * @returns {string|string[]} 返回图片base64或数组 - */ diff --git a/lib/renderer/Server.js b/lib/renderer/Server.js deleted file mode 100644 index 264f982..0000000 --- a/lib/renderer/Server.js +++ /dev/null @@ -1,129 +0,0 @@ -import Renderer from './App.js' -import RenderBase from './Base.js' -import { randomUUID } from 'crypto' - -class Puppeteer extends RenderBase { - /** - * @type {import('../index.js').logger} - */ - #logger - - /** - * @type {import('../index.js').common} - */ - #common - - /** - * @type {import('../index.js').listener} - */ - #listener - - constructor (logger, common, listener) { - super() - this.index = 0 - this.#logger = logger - this.#common = common - this.#listener = listener - } - - async server (socket, request) { - this.socket = socket - this.request = request - this.id = this.request.headers['renderer-id'] - this.type = this.request.headers['renderer-type'] - - /** 注册渲染器 */ - this.host = this.request.headers.host - this.url = `ws://${this.host + this.request.url}` - - this.#logger.info(`[渲染器:${this.id}] 收到新的连接请求:` + this.#logger.green(this.url)) - /** 监听上报事件 */ - this.socket.on('message', data => { - data = JSON.parse(data) - if (data.echo) { - this.#listener.emit(data.echo, data) - } else if (data.action === 'heartbeat') { - this.#logger.debug(`[渲染器:${this.id}] 收到心跳:${this.url}`) - } else { - this.#logger.warn(`[渲染器:${this.id}] 收到未知数据:`, data) - } - }) - - /** 监听断开 */ - this.socket.on('close', () => { - this.#logger.warn(`[渲染器:${this.id}] 连接断开:${this.url}`) - /** 卸载渲染器 */ - this.index && Renderer.unapp(this.index) - this.index = 0 - }) - - /** 注册渲染器 */ - try { - const index = Renderer.app({ - id: this.id, - type: this.type, - render: this.render.bind(this), - }) - this.index = index - } catch (error) { - this.#logger.error(`[渲染器:${this.id}] 注册渲染器失败:`, error) - /** 断开连接 */ - this.socket.close() - } - } - - /** - * 渲染模板 - * @param {object} options 渲染参数 - * @param {string} options.file http地址或本地文件路径 - * @param {string} [options.name] 模板名称 - * @param {string} [options.fileID] art-template后的文件名 - * @param {object} [options.data] 传递给模板的数据 template.render(data) - * @param {'png'|'jpeg'|'webp'} [options.type] 截图类型 默认'webp' - * @param {number} [options.quality] 截图质量 默认90 1-100 - * @param {boolean} options.omitBackground 是否隐藏背景 默认false - * @param {object} [options.setViewport] 设置视窗大小和设备像素比 默认1920*1080、1 - * @param {number} [options.setViewport.width] 视窗宽度 - * @param {number} [options.setViewport.height] 视窗高度 - * @param {string} [options.setViewport.deviceScaleFactor] 设备像素比 - * @param {number|boolean} [options.multiPage] 分页截图 传递数字则视为视窗高度 返回数组 - * @param {object} [options.pageGotoParams] 页面goto时的参数 - * @param {number} [options.pageGotoParams.timeout] 页面加载超时时间 - * @param {'load'|'domcontentloaded'|'networkidle0'|'networkidle2'} [options.pageGotoParams.waitUntil] 页面加载状态 - * @returns {Promise} 返回图片base64或数组 - */ - async render (options) { - /** 渲染模板 */ - let file = '' - - if (options.file.includes('http') || options.vue) { - file = options.file - } else { - file = 'file://' + this.dealTpl(options) - } - - if (!file) throw new Error(`[渲染器:${this.id}] 模板文件不存在:${options.name}`) - - const echo = randomUUID() - const action = 'render' - const data = options - /** 移除掉模板参数 */ - if (data.data) delete data.data - data.file = file - this.#logger.debug(`[渲染器:${this.id}][反向WS] \n请求:${this.url} \nhtml: ${options.file} \ndata: ${JSON.stringify(data)}`) - this.socket.send(JSON.stringify({ echo, action, data })) - - return new Promise((resolve, reject) => { - this.#listener.once(echo, data => { - if (data.ok) return resolve(data.data) - reject(new Error(JSON.stringify(data))) - }) - }) - } -} - -export default { - type: 'render', - path: '/puppeteer', - adapter: Puppeteer, -} diff --git a/lib/renderer/Wormhole.js b/lib/renderer/Wormhole.js deleted file mode 100644 index 4b4db1f..0000000 --- a/lib/renderer/Wormhole.js +++ /dev/null @@ -1,166 +0,0 @@ -import WebSocket from 'ws' -import fs from 'fs' -import path from 'path' -import { URL } from 'url' -import Renderer from './App.js' -import HttpRenderer from './Http.js' - -let ws -let reConnect -const chunkSize = 1024 * 1024 * 3 // 文件分片大小 - -export default function connect (Cfg) { - let heartbeat - let index = 0 - reConnect = undefined - const wsUrl = Cfg.Server.HttpRender.WormholeClient - ws = new WebSocket(wsUrl) - ws.on('open', function open () { - logger.info('连接到wormhole服务器' + wsUrl) - // 发送心跳 - heartbeat = setInterval(() => { - ws.send(JSON.stringify({ type: 'heartbeat', date: new Date() })) - }, 30000) // 每30秒发送一次心跳 - }) - - ws.on('message', function incoming (data) { - try { - data = JSON.parse(data) - } catch (error) { - logger.warn(`收到非法消息${data}`) - } - const echo = data.echo - switch (data.type) { - case 'msg': { - const { post, token, WormholeClient } = Cfg.Server.HttpRender - const parsedUrl = new URL(WormholeClient) - const { hostname, port } = parsedUrl - const ishttps = WormholeClient.includes('wss://') - const host = `${ishttps ? 'https' : 'http'}://${hostname}${port ? `:${port}` : ''}/web/${data.date}` - logger.mark(`web渲染器已连接,地址:${host}`) - /** 注册渲染器 */ - const rd = new HttpRenderer(host, post, token) - index = Renderer.app({ id: 'puppeteer', type: 'image', render: rd.render.bind(rd) }) - break - } - case 'web': - if (data.path) { - const filePath = data.path - const query = data.query - if (query.html) { - ws.send(JSON.stringify({ type: 'web', command: 'redirect', path: filePath, target: query.html.startsWith('/') ? query.html.slice(1) : query.html, echo })) - return - } - const list = ['.css', '.html', '.ttf', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.woff', '.woff2'] - if (!list.some(ext => path.extname(filePath).endsWith(ext))) { - logger.warn(`拦截非资源文件${filePath}`) - ws.send(JSON.stringify({ type: 'web', state: 'error', error: '非资源文件', echo })) - return - } - - /** 判断一下是否为html 如果是需要特殊处理 */ - if (path.extname(filePath) === '.html') { - let html = filePath - /** 获取文件路径 对路径进行处理,去掉../、./ */ - html = `./${html.replace(/\\/g, '/').replace(/(\.\/|\.\.\/)/g, '')}` - - /** 判断是否为html文件且路径存在 */ - if (!fs.existsSync(html)) { - return ws.send(JSON.stringify({ type: 'web', state: 'error', error: '文件不存在', echo })) - } - - let content = fs.readFileSync(html, 'utf-8') - /** 处理所有绝对路径、相对路径 */ - content = content.replace(new RegExp(`(${process.cwd()}|${process.cwd().replace(/\\/g, '/')})`, 'g'), '') - // 保存到本地 - // filePath = './1.html' - // fs.writeFileSync(filePath, content, 'utf-8') - return ws.send(JSON.stringify({ - type: 'web', - path: data.path, - command: 'resource', - data: Buffer.from(content), - state: 'complete', - part: 0, - echo, - })) - } - - logger.info(`获取网页文件数据:${filePath}`) - // 获取文件 - - const stream = fs.createReadStream(filePath, { highWaterMark: chunkSize }) - let part = 0 - - stream.on('data', (chunk) => { - part++ - const message = { - type: 'web', - path: data.path, - command: 'resource', - data: chunk, - state: 'part', - part, - echo, - } - - ws.send(JSON.stringify(message)) - }) - - stream.on('end', () => { - part++ - // 如果是最后一片段,则更新状态为 'complete' - if (stream.readableEnded) { - ws.send(JSON.stringify({ - type: 'web', - path: data.path, - command: 'resource', - data: '', - state: 'complete', - part, - echo, - })) - } - }) - - stream.on('error', (err) => { - ws.send(JSON.stringify({ type: 'web', state: 'error', error: err.message, echo })) - }) - } else { - ws.send(JSON.stringify({ type: 'web', state: 'error', error: '错误的文件路径', echo })) - } - break - default: - logger.warn(`未知消息类型${JSON.stringify(data)}`) - break - } - }) - - ws.on('close', function close () { - /** 卸载渲染器 */ - index && Renderer.unapp(index) - index = 0 - if (heartbeat) { - clearInterval(heartbeat) - heartbeat = null - } - logger.warn('连接关闭,10秒后尝试重新连接') - if (!reConnect) { - reConnect = setTimeout(connect, 10000) - } - }) - - ws.on('error', function error () { - /** 卸载渲染器 */ - index && Renderer.unapp(index) - index = 0 - if (heartbeat) { - clearInterval(heartbeat) - heartbeat = null - } - logger.warn('连接错误,10秒后尝试重新连接') - if (!reConnect) { - reConnect = setTimeout(connect, 10000) - } - }) -} diff --git a/lib/tools/install.js b/lib/tools/install.js deleted file mode 100644 index 8970f79..0000000 --- a/lib/tools/install.js +++ /dev/null @@ -1,180 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { exec } from 'child_process' - -class Install { - constructor () { - this.dir = process.cwd() - } - - /** - * npm install - */ - async init () { - this.plugins = this.getPluginsPath() - - // const dependenciesMap = { - // // 5.1.7版本很多人可能无法安装 暂时固定为5.1.6 - // sqlite3: '5.1.6', - // sequelize: '^6.37.3', - // redis: '^4.6.14', - // } - - // const install = [] - - // for (const plugin of this.plugins) { - // const packagePath = plugin + '/package.json' - // const data = this.package(packagePath) - // if (!data.karin || !data.karin.db || !Array.isArray(data.karin.db)) continue - // for (const name of data.karin.db) { - // /** 数组长度===4 */ - // if (install.length === 4) break - - // switch (name) { - // case 'sequelize': { - // if (!install.includes('sequelize')) install.push('sequelize') - // continue - // } - // case 'sqlite3': { - // if (!install.includes('sqlite3')) install.push('sqlite3') - // continue - // } - // case 'redis': { - // if (!install.includes('redis')) install.push('redis') - // continue - // } - // default: { - // console.error(`[install] 未知数据库插件:${name}`) - // continue - // } - // } - // } - // } - - // console.log('[install] 需要安装的依赖:', install) - - // const json = { - // name: 'karin-driver-db', - // type: 'module', - // dependencies: {}, - // } - - // for (const key in install) { - // const name = install[key] - // json.dependencies[name] = dependenciesMap[name] - // } - - // const driverPath = this.dir + '/plugins/karin-driver-db' - - // // 写入package.json - // fs.writeFileSync(driverPath + '/package.json', JSON.stringify(json, null, 2)) - // if (process.env.npm_lifecycle_event === 'init:pack') { - // console.log('[install] 当前为生成包模式,不执行安装依赖,任务结束') - // return - // } - - // this.plugins.push(driverPath) - await this.install() - } - - /** - * 获取所有插件绝对路径 - */ - getPluginsPath () { - const files = fs.readdirSync(this.dir + '/plugins', { withFileTypes: true }) - // 过滤掉非karin-plugin-开头或karin-adapter-开头的文件夹 - let plugins = files.filter(file => file.isDirectory() && (file.name.startsWith('karin-plugin-') || file.name.startsWith('karin-adapter-'))).map(dir => dir.name) - // 排除没有package.json的插件 - plugins = plugins.filter(plugin => fs.existsSync(this.dir + '/plugins/' + plugin + '/package.json')) - return plugins.map(plugin => this.dir + '/plugins/' + plugin) - } - - /** - * 创建目录 - * @param {string} Path - 路径 - */ - mkdir (Path) { - if (fs.existsSync(Path)) return true - /** 递归自调用 */ - if (this.mkdir(path.dirname(Path))) fs.mkdirSync(Path) - return true - } - - /** - * 解析package.json - * @param {string} Path - package.json路径 - */ - package (Path) { - const data = JSON.parse(fs.readFileSync(Path, 'utf8')) - return data - } - - /** - * 执行安装依赖 - */ - async install () { - const { npm_lifecycle_event: lifecycleEvent, npm_config_user_agent: agent, npm_config_userconfig: userconfig } = process.env - const isInit = lifecycleEvent === 'init' - const baseCmd = isInit ? 'install -P' : 'install' - let cmd - - if (agent && agent.includes('pnpm')) { - cmd = `pnpm ${baseCmd}` - await this.installDependencies(cmd, '[pnpm]') - } else if (userconfig && userconfig.includes('cnpm')) { - cmd = `cnpm ${baseCmd}` - await this.installDependencies(cmd, '[cnpm]', true) - } else if (agent && agent.includes('yarn')) { - cmd = `yarn ${baseCmd}` - await this.installDependencies(cmd, '[yarn]') - } else { - cmd = `npm ${baseCmd}` - await this.installDependencies(cmd, '[npm]', true) - } - } - - /** - * 安装依赖并打印日志 - * @param {string} cmd - 要执行的命令 - * @param {string} label - 日志标签 - * @param {boolean} installPlugins - 是否安装插件依赖 - */ - async installDependencies (cmd, label, installPlugins = false) { - console.log(`[install]${label} 开始安装依赖: ${cmd}`) - const res = await this.execPromise(cmd, this.dir) - console.log(res) - - if (installPlugins) { - await Promise.all(this.plugins.map(async (plugin) => { - const pluginCmd = `${cmd} ${path.basename(plugin)}` - console.log(`${label} 开始安装插件依赖: ${pluginCmd}`) - const res = await this.execPromise(pluginCmd, plugin) - console.log(res) - console.log(`${label} ${plugin} 依赖安装完成`) - })) - } - - console.log(`${label} 依赖安装完成,任务结束`) - } - - /** - * 执行命令 - * @param {string} command - 命令 - * @param {string} cwd - 执行路径 - * @returns {Promise} - */ - async execPromise (command, cwd) { - return new Promise((resolve, reject) => { - exec(command, { env: process.env, cwd, silent: true }, (error, stdout) => { - if (error) { - reject(error) - } else { - resolve(stdout) - } - }) - }) - } -} - -const install = new Install() -install.init() diff --git a/lib/tools/pm2Log.js b/lib/tools/pm2Log.js deleted file mode 100644 index 9eb756c..0000000 --- a/lib/tools/pm2Log.js +++ /dev/null @@ -1,14 +0,0 @@ -import fs from 'fs' -import yaml from 'yaml' -import path from 'path' -import { fileURLToPath } from 'url' -import { spawn } from 'child_process' - -const dir = path.resolve(fileURLToPath(import.meta.url), '../../..') -const _path = path.resolve(dir + '/config/config/pm2.yaml') -const data = yaml.parse(fs.readFileSync(_path, 'utf8')) - -const name = data.apps[0].name -const lines = data.lines || 1000 -const cmd = process.platform === 'win32' ? 'pm2.cmd' : 'pm2' -spawn(cmd, ['logs', '--lines', lines, name], { stdio: 'inherit', shell: true, cwd: dir }) diff --git a/lib/tools/uninstall.js b/lib/tools/uninstall.js deleted file mode 100644 index 148f21b..0000000 --- a/lib/tools/uninstall.js +++ /dev/null @@ -1,64 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { exec } from 'child_process' - -class Uninstall { - constructor () { - this.dir = process.cwd() - } - - /** - * 循环删除依赖文件夹 - */ - async init () { - const list = this.getPluginsPath() - console.log(`[uninstall][数量:${list.length}] 开始删除依赖文件夹`) - const cmd = process.platform === 'win32' ? 'rd/s/q node_modules' : 'rm -rf node_modules' - if (fs.existsSync(this.dir + '/node_modules')) { - console.log('[uninstall] 删除node_modules文件夹') - await this.execPromise(cmd, this.dir) - } - for (const plugin of list) { - if (fs.existsSync(plugin + '/node_modules')) { - console.log(`[uninstall][${path.basename(plugin)}] 删除node_modules文件夹`) - await this.execPromise(cmd, plugin) - } - } - console.log('[uninstall] 依赖文件夹删除完成') - } - - /** - * 获取所有插件绝对路径 - */ - getPluginsPath () { - const files = fs.readdirSync(this.dir + '/plugins', { withFileTypes: true }) - // 过滤掉非karin-plugin-开头或karin-adapter-开头的文件夹 - let plugins = files.filter(file => file.isDirectory() && (file.name.startsWith('karin-plugin-') || file.name.startsWith('karin-adapter-'))).map(dir => dir.name) - // 排除没有package.json的插件 - plugins = plugins.filter(plugin => fs.existsSync(this.dir + '/plugins/' + plugin + '/package.json')) - // 排除没有node_modules的插件 - plugins = plugins.filter(plugin => fs.existsSync(this.dir + '/plugins/' + plugin + '/node_modules')) - return plugins.map(plugin => this.dir + '/plugins/' + plugin) - } - - /** - * 执行命令 - * @param {string} command - 命令 - * @param {string} cwd - 执行路径 - * @returns {Promise} - */ - async execPromise (command, cwd) { - return new Promise((resolve, reject) => { - exec(command, { env: process.env, cwd, silent: true }, (error, stdout) => { - if (error) { - reject(error) - } else { - resolve(stdout) - } - }) - }) - } -} - -const uninstall = new Uninstall() -uninstall.init() diff --git a/lib/tools/updateVersion.js b/lib/tools/updateVersion.js deleted file mode 100644 index 9b87fa2..0000000 --- a/lib/tools/updateVersion.js +++ /dev/null @@ -1,5 +0,0 @@ -import Version from '../utils/updateVersion.js' - -const path = process.cwd() -const version = new Version(path) -await version.init() diff --git a/lib/utils/YamlEditor.js b/lib/utils/YamlEditor.js deleted file mode 100644 index 4f3d69a..0000000 --- a/lib/utils/YamlEditor.js +++ /dev/null @@ -1,212 +0,0 @@ -/* - * YamlEditor - 一个用于编辑 YAML 文件的类 - * 本代码由 ChatGPT4 提供 - * https://github.com/OpenAI - */ -import fs from 'fs' -import Yaml from 'yaml' -import lodash from 'lodash' - -export default class YamlEditor { - constructor (filePath) { - this.filePath = filePath - this.document = null - this.load() - } - - load () { - try { - const fileContents = fs.readFileSync(this.filePath, 'utf8') - this.document = Yaml.parseDocument(fileContents) - logger.debug('[YamlEditor] 文件加载成功') - } catch (error) { - logger.error(`[YamlEditor] 加载文件时出错:${error}`) - } - } - - /** - * 获取指定路径的值 - * @param {string} path - 路径,用点号分隔,例如:'a.b.c' - * @returns {any} - */ - get (path) { - try { - if (!path) return this.document.toJSON() - return lodash.get(this.document.toJSON(), path) - } catch (error) { - logger.error(`[YamlEditor] 获取数据时出错:${error}`) - return null - } - } - - /** - * 设置指定路径的值 - * @param {string} path - 路径,用点号分隔,例如:'a.b.c' - * @param {any} value - 要设置的值 - */ - set (path, value) { - try { - path = path.split('.') - this.document.setIn(path, value) - } catch (error) { - logger.error(`[YamlEditor] 设置数据时出错:${error}`) - return null - } - } - - /** - * 向指定路径添加新值 - * @param {string} path - 路径,用点号分隔,例如:'a.b.c' - * @param {any} value - 要添加的值 - */ - add (path, value) { - try { - path = path.split('.') - this.document.addIn(path, value) - logger.debug(`[YamlEditor] 已在 ${path} 添加新的值`) - } catch (error) { - logger.error(`[YamlEditor] 添加数据时出错:${error}`) - } - } - - /** - * 删除指定路径 - * @param {string} path - 路径,用点号分隔,例如:'a.b.c' - * @returns {boolean} 是否删除成功 - */ - del (path) { - try { - path = path.split('.') - this.document.deleteIn(path) - return true - } catch (error) { - logger.error(`[YamlEditor] 删除数据时出错:${error}`) - return false - } - } - - /** - * 向指定路径的数组添加新值,可以选择添加到数组的开始或结束 - * @param {string} path - 路径,用点号分隔,例如:'a.b.c' - * @param {string|object|Array} value - 要添加的值 - * @param {boolean} [prepend=false] - 如果为 true,则添加到数组的开头,否则添加到末尾 - */ - append (path, value, prepend = false) { - try { - path = path.split('.') || [] - let current = this.document.getIn(path) - if (!current) { - current = new Yaml.YAMLSeq() - this.document.setIn(path, current) - } else if (!(current instanceof Yaml.YAMLSeq)) { - throw new Error('[YamlEditor] 指定的路径不是数组') - } else { - if (prepend) { - current.items.unshift(value) - } else { - current.add(value) - } - } - logger.debug(`[YamlEditor] 已向 ${path} 数组${prepend ? '开头' : '末尾'}添加新元素:${value}`) - } catch (error) { - logger.error(`[YamlEditor] 向数组添加元素时出错:${error}`) - } - } - - /** - * 检查指定路径的键是否存在 - * @param {string} path - 路径,用点号分隔 - * @returns {boolean} - */ - has (path) { - try { - path = path.split('.') - return this.document.hasIn(path) - } catch (error) { - logger.error(`[YamlEditor] 检查路径是否存在时出错:${error}`) - return false - } - } - - /** - * 查询指定路径中是否包含指定的值 - * @param {string} path - 路径,用点号分隔 - * @param {any} value - 要查询的值 - * @returns {boolean} - */ - hasVal (path, value) { - try { - path = path.split('.') - const current = this.document.getIn(path) - if (!current) return false - - /** 检查当前节点是否包含指定的值 */ - if (current instanceof Yaml.YAMLSeq) { - /** 如果是序列,遍历序列查找值 */ - return current.items.some(item => lodash.isEqual(item.toJSON(), value)) - } else if (current instanceof Yaml.YAMLMap) { - /** 如果是映射,检查每个值 */ - return Array.from(current.values()).some(v => lodash.isEqual(v.toJSON(), value)) - } else { - /** 否则,直接比较值 */ - return lodash.isEqual(current, value) - } - } catch (error) { - logger.error(`[YamlEditor] 检查路径 ${path} 是否包含值时出错:${error}`) - return false - } - } - - /** - * 向根节点新增元素,如果根节点不是数组,则将其转换为数组再新增元素 - * @param {any} value - 要新增的元素 - */ - pusharr (value) { - try { - if (!(this.document.contents instanceof Yaml.YAMLSeq)) { - // 如果根节点不是数组,则将其转换为数组 - this.document.contents = new Yaml.YAMLSeq() - logger.debug('[YamlEditor] 根节点已转换为数组') - } - this.document.contents.add(value) - logger.debug('[YamlEditor] 已向根节点数组新增元素:', value) - } catch (error) { - logger.error(`[YamlEditor] 向根节点数组新增元素时出错:${error}`) - return false - } - } - - /** - * 根据索引从根节点数组删除元素 - * @param {number} index - 要删除元素的索引 - * @returns {boolean} 是否删除成功 - */ - delarr (index) { - try { - if (!(this.document.contents instanceof Yaml.YAMLSeq)) { - throw new Error('[YamlEditor] 根节点不是数组') - } - if (index < 0 || index >= this.document.contents.items.length) { - throw new Error('[YamlEditor] 索引超出范围') - } - this.document.contents.items.splice(index, 1) - logger.debug('[YamlEditor] 已根据索引从根节点数组删除元素,索引:', index) - return true - } catch (error) { - logger.error(`[YamlEditor] 根据索引删除根节点数组元素时出错:${error}`) - return false - } - } - - /** - * 保存文件 - */ - save () { - try { - fs.writeFileSync(this.filePath, this.document.toString()) - logger.info('[YamlEditor] 文件已保存') - } catch (error) { - logger.error(`[YamlEditor] 保存文件时出错:${error}`) - } - } -} diff --git a/lib/utils/app.js b/lib/utils/app.js deleted file mode 100644 index 4249e6e..0000000 --- a/lib/utils/app.js +++ /dev/null @@ -1,163 +0,0 @@ -import lodash from 'lodash' -import plugin from '../plugins/plugin.js' - -export default class App { - constructor ({ name = '插件名称', dsc = '', event = 'message', priority = 5000, task = [], rule = [] }) { - this.name = name - this.dsc = dsc || name - this.event = event - this.priority = priority - this.rule = rule - this.task = task - } - - /** - * karin插件构建器 - * @param {object} params - 插件配置对象 - * @param {string} params.name - 插件名称 - * @param {string} [params.dsc='描述'] - 插件描述 - * @param {string} [params.event='message'] - 监听事件 - * @param {number} [params.priority=5000] - 插件优先级 - * @param {object} [params.task={ fnc: '', cron: '' }[]] - 插件任务 - * @param {Array} [params.rule=[]] - 插件规则 - * @returns {App} - 返回插件对象 - */ - static init (params) { - return new App(params) - } - - /** - * 注册插件规则 - * @param {Object} rule - 插件规则对象 - * @param {string|Function} rule.fnc - 插件函数名或函数 - * @param {string} [rule.reg=''] - 插件规则 - * @throws {Error} 如果缺少fnc或fnc类型错误 - * @throws {Error} 如果指定的方法不存在 - */ - reg (rule) { - /** 判断是否传入reg和fnc */ - if (!rule.fnc) throw new Error('[插件构建] 缺少fnc') - /** 如果fnc是字符串,则在传入的对象中查找对应的方法 */ - if (typeof rule.fnc === 'string') { - if (!rule[rule.fnc]) throw new Error(`[插件构建] ${rule.fnc} 方法不存在`) - this[rule.fnc] = rule[rule.fnc] - /** 删除掉这个函数 */ - delete rule[rule.fnc] - } else if (typeof rule.fnc === 'function') { - /** 随机生成一个方法名 */ - const fnc_name = `fnc_${Math.random().toString(36)}` - const fnc = rule.fnc - this[fnc_name] = fnc - rule.fnc = fnc_name - delete rule[fnc_name] - } - - /** 将函数注册到当前对象中 */ - lodash.forIn(rule, (value, key) => { - if (typeof value === 'function') this[key] = value - }) - - this.rule.push({ - fnc: rule.fnc, - reg: rule.reg || '', - log: rule.log || true, - permission: rule.permission || 'all', - }) - } - - /** - * 将函数或函数对象注册到当前对象中 - * @param {string} name - 函数名 - * @param {Function|Object} fnc - 要注册的函数或函数对象 - */ - fnc (name, fnc) { - if (!name) throw new Error('[插件构建] 缺少name') - if (typeof fnc !== 'function') throw new Error('[插件构建] fnc类型错误') - this[name] = fnc - } - - /** - * 设置插件接收函数 - * @param {Function} fnc - 接收函数 - */ - accept (fnc) { - if (typeof fnc === 'function') { - this.accept = fnc - } - } - - /** - * 新增插件定时任务 - * @param {{ - * name: string, - * cron: string, - * fnc: string|Function, - * log?: boolean, - * }} task - 插件定时任务对象 - * @param {string} task.name - 定时任务名称 - * @param {string} task.cron - 定时任务表达式 - * @param {string|Function} task.fnc - 定时任务函数名或函数 - * @param {boolean} [task.log=true] - 是否记录日志 - */ - cron (task) { - /** 检查传入的参数是否符合规则 */ - if (!task.name) throw new Error('[插件构建][定时任务] 缺少name') - if (!task.cron) throw new Error('[插件构建][定时任务] 缺少cron') - if (!task.fnc) throw new Error('[插件构建][定时任务] 缺少fnc') - if (typeof task.fnc !== 'function') throw new Error('[插件构建][定时任务] fnc类型错误,必须为function') - - /** 如果fnc是字符串,则在传入的对象中查找对应的方法 */ - if (typeof task.fnc === 'string') { - this[task.fnc] = task[task.fnc] - } else if (typeof task.fnc === 'function') { - /** 随机生成一个方法名 */ - const fnc_name = `fnc_${Math.random().toString(36)}` - const fnc = task.fnc - this[fnc_name] = fnc - task.fnc = fnc_name - } - - /** 将函数注册到当前对象中 */ - lodash.forIn(task, (value, key) => { - if (typeof value === 'function') this[key] = value - }) - - this.task.push({ - name: task.name, - cron: task.cron, - fnc: task.fnc, - log: task.log || true, - }) - } - - /** - * 创建插件类 - * @param {Object} app - 插件配置对象 - * @param {string} app.name - 插件名称 - * @param {string} app.dsc - 插件描述 - * @param {string} app.event - 插件事件 - * @param {number} app.priority - 插件优先级 - * @param {string} app.task - 插件任务 - * @param {Object} app.rule - 插件规则对象 - * @returns {Class} - 返回创建的插件类 - */ - plugin (app) { - const cla = class extends plugin { - constructor () { - super({ - name: app.name, - dsc: app.dsc, - event: app.event, - priority: app.priority, - task: app.task, - rule: app.rule, - }) - } - } - // 循环app中的所有函数,构建到cla的原型中 - lodash.forIn(app, (value, key) => { - if (typeof value === 'function') cla.prototype[key] = value - }) - return cla - } -} diff --git a/lib/utils/button.js b/lib/utils/button.js deleted file mode 100644 index f5b373e..0000000 --- a/lib/utils/button.js +++ /dev/null @@ -1,91 +0,0 @@ -import util from 'util' -import lodash from 'lodash' - -export default class Button { - constructor () { - /** - * @type {Array<{ - * App: class, - * name: string, - * priority: number, - * file: { name: string, dir: string }, - * rule: Array<{ reg: RegExp, fnc: string }> - * }>} - */ - this.Apps = [] - } - - add ({ name, dir, App, Class }) { - const rule = [] - /** 创建正则表达式 */ - for (const v of Class.button) { - try { - let { reg, fnc } = v - reg = new RegExp(reg) - rule.push({ reg, fnc }) - } catch (error) { - logger.error(error) - continue - } - } - - this.Apps.push({ - App, - name: Class.name, - priority: Class.priority, - file: { name, dir }, - rule, - }) - } - - /** - * 卸载按钮 - * @param {string} dir 插件目录 - * @param {string} name 插件文件名称 - */ - del (dir, name) { - /** 未传入name则删除所有 */ - if (!name) { - this.Apps = this.Apps.filter(v => v.file.dir !== dir) - } else { - /** 传入name则删除指定 */ - this.Apps = this.Apps.filter(v => v.file.dir !== dir || v.file.name !== name) - } - /** 排序 */ - this.Apps = lodash.orderBy(this.Apps, ['priority'], ['asc']) - return this.Apps - } - - update ({ name, dir, App, Class }) { - this.del(dir, name) - this.add({ name, dir, App, Class }) - } - - async get (e) { - const button = [] - for (const app of this.Apps) { - for (const v of app.rule) { - /** 这里的lastIndex是为了防止正则无法从头开始匹配 */ - v.reg.lastIndex = 0 - if (v.reg.test(e.msg)) { - try { - const App = new app.App() - App.e = e - let res = App[v.fnc] && App[v.fnc](e) - if (util.types.isPromise(res)) res = await res - if (!res) continue - /** 是否继续循环 */ - const cycle = res.cycle ?? true - delete res.cycle - button.push(res) - if (cycle !== false) return button - } catch (error) { - logger.error(error) - } - } - } - } - /** 理论上不会走到这里,但是还是要稳一手,不排除有所有插件都false... */ - return button - } -} diff --git a/lib/utils/exec.js b/lib/utils/exec.js deleted file mode 100644 index 525b309..0000000 --- a/lib/utils/exec.js +++ /dev/null @@ -1,50 +0,0 @@ -import { exec as execCmd } from 'child_process' - -/** - * 执行 shell 命令 - * @param {string} cmd - shell 命令 - * @param {boolean} [log=true] - 是否输出日志 - * @param {import('child_process').ExecOptions} [options={ cwd: process.cwd(), encoding: 'utf-8' }] - exec 选项 - * @returns {Promise<{ status: 'ok'|'failed', stdout: string, stderr: string, error: Error }>} - 执行结果 - */ -export const exec = (cmd, log = true, options = { cwd: process.cwd(), encoding: 'utf-8' }) => { - return new Promise((resolve) => { - const logger = global.logger || console - - const logMessage = (level, message) => { - if (log) logger[level](message) - } - - const logType = (status) => { - switch (status) { - case '开始执行': - return logger.yellow('[exec][开始执行]') - case 'ok': - return logger.green('[exec][执行成功]') - case 'failed': - return logger.red('[exec][执行失败]') - } - } - - const formatMessage = (status, details) => [ - logType(status), - `cmd: ${cmd}`, - `cwd: ${options.cwd || process.cwd()}`, - details, - '--------', - ].join('\n') - - logMessage('info', formatMessage('开始执行', '')) - - execCmd(cmd, options, (error, stdout, stderr) => { - if (error) { - logMessage('error', formatMessage('failed', `Error: ${error.message || error.stack || error.toString()}`)) - return resolve({ status: 'failed', error, stdout, stderr }) - } - logMessage('mark', formatMessage('ok', `stdout: ${stdout}\nstderr: ${stderr}`)) - resolve({ status: 'ok', error, stdout, stderr }) - }) - }) -} - -export default exec diff --git a/lib/utils/ffmpeg.js b/lib/utils/ffmpeg.js deleted file mode 100644 index 2063602..0000000 --- a/lib/utils/ffmpeg.js +++ /dev/null @@ -1,26 +0,0 @@ -import exec from './exec.js' - -/** - * 执行 ffmpeg 命令 - * @returns {Promise<(cmd: string) => Promise>} - */ -export default async function ffmpeg (logger, config) { - let ffmpeg = 'ffmpeg' - const { status } = await exec('ffmpeg -version', false) - if (status !== 'ok') { - logger.debug('ffmpeg 未安装,开始尝试读取配置文件是否存在ffmpeg路径') - const ffmpegPath = config.Config.ffmpeg_path - if (!ffmpegPath) { - logger.warn('ffmpeg 未安装,请安装 ffmpeg 或手动配置 ffmpeg 路径后重启') - return false - } - ffmpeg = `"${ffmpegPath}"` - } - - // 返回函数 - return async (cmd, log = true, options = { cwd: process.cwd(), encoding: 'utf-8' }) => { - cmd = cmd.replace(/^ffmpeg/, '').trim() - cmd = `${ffmpeg} ${cmd}` - return await exec(cmd, log, options) - } -} diff --git a/lib/utils/handler.js b/lib/utils/handler.js deleted file mode 100644 index f21c4bb..0000000 --- a/lib/utils/handler.js +++ /dev/null @@ -1,120 +0,0 @@ -import util from 'util' -import lodash from 'lodash' - -/** - * 事件处理器类 - */ -export default class EventHandler { - /** - * 创建一个事件处理器实例 - */ - constructor () { - /** @type {Object} 事件处理器映射表 */ - this.events = {} - } - - /** - * 添加事件处理器 - * @param {Object} config 配置对象 - * @param {string} config.name 处理器名称 - * @param {string} config.dir 处理器所在目录 - * @param {Function} config.App 应用构造函数 - * @param {Object} config.Class 类配置 - */ - add ({ name, dir, App, Class }) { - for (const cfg of Class.handler) { - const { key = '', fnc = '', priority = 2000 } = cfg - if (!key) { - return logger.error(`[Handler][Add]: [${name}] 缺少 key`) - } - if (!fnc) { - return logger.error(`[Handler][Add]: [${name}] 缺少 fnc`) - } - logger.debug(`[Handler][Reg]: [${name}][${key}]`) - if (!Array.isArray(this.events[key])) this.events[key] = [] - this.events[key].push({ file: { name, dir }, App, key, fnc, priority }) - this.events[key] = lodash.orderBy(this.events[key], ['priority'], ['asc']) - } - } - - /** - * 删除事件处理器 - * @param {{ - * dir: string, - * name: string, - * key?: string - * }} config 配置对象 - * @param {string} config.dir 目录 - * @param {string} config.name 名称 - * @param {string} [config.key] 事件键 未传入则删除所有处理器 - */ - del ({ dir, name, key = '' }) { - /** 这里是删除所有 全部重新初始化 */ - if (!key && !dir && !name) { - this.events = {} - return true - } - - /** 热重载 删除指定目录 */ - if (!key) { - for (const v of Object.keys(this.events)) { - this.events[v] = this.events[v].filter(v => v.file.dir !== dir || v.file.name !== name) - // 如果处理器为空则删除键 - if (!this.events[v].length) { - delete this.events[v] - } else { - this.events[v] = lodash.orderBy(this.events[v], ['priority'], ['asc']) - } - } - return true - } - - if (!this.events[key]) return false - this.events[key] = this.events[key].filter(v => v.file.dir !== dir || v.file.name !== name) - this.events[key] = lodash.orderBy(this.events[key], ['priority'], ['asc']) - return true - } - - /** - * 调用事件处理器 - * @param {string} key 事件键 - * @param {Object} args 参数数组 - * @returns {Promise} 处理结果 - */ - async call (key, args = {}) { - let ret - for (const v of this.events[key] || []) { - const App = new v.App() - if (args.e) App.e = args.e - let done = true - // 标记函数,用于标记处理器是否执行成功,由处理器自行调用,如果未调用则认为处理器未执行成功 - const reject = (msg = '') => { - if (msg) { - logger.mark(`[Handler][Reject]: [${v.file.dir}][${v.file.name}][${key}] ${msg}`) - } - done = false - } - try { - ret = App[v.fnc] && App[v.fnc](args, reject) - if (util.types.isPromise(ret)) ret = await ret - if (done) { - logger.mark(`[Handler][Done]: [${v.file.dir}][${v.file.name}][${key}]`) - return ret - } - } catch (e) { - // 产生错误继续下一个处理器 - logger.error(`[Handler][Error]: [${v.file.dir}][${v.file.name}][${key}] ${e}`) - } - } - return ret - } - - /** - * 检查是否存在指定键的事件处理器 - * @param {string} key 事件键 - * @returns {boolean} 存在返回 true,否则返回 false - */ - has (key) { - return !!this.events[key] - } -} diff --git a/lib/utils/protobuf.js b/lib/utils/protobuf.js deleted file mode 100644 index 4c7d092..0000000 --- a/lib/utils/protobuf.js +++ /dev/null @@ -1,282 +0,0 @@ -/* - * protobuf - * 此文件来自于icqq - * icqq/blob/main/src/core/protobuf/index.ts - */ - -// import * as pb from "protobufjs" -import { protobufjs as pb } from 'kritor-proto' -import * as zlib from 'zlib' - -export class Proto { - get length () { - return this.encoded.length - } - - constructor (encoded, decoded) { - this.encoded = encoded - if (decoded) { - Reflect.setPrototypeOf(this, decoded) - } - } - - toString () { - return this.encoded.toString() - } - - toHex () { - return this.encoded.toString('hex') - } - - toBase64 () { - return this.encoded.toString('base64') - } - - toBuffer () { - return this.encoded - } - - toJSON () { - const toJSON = (pb) => { - if (!(pb instanceof Proto)) return pb - const keys = Object.keys(pb) - if (keys.length === 1 && keys[0] === 'encoded') { - try { - pb = decode(pb.encoded) - } catch { - return pb.encoded.toString() - } - } - if (!pb) return pb - const result = {} - for (const k of Object.keys(pb)) { - if (!/^\d+$/.test(k)) continue - const key = Number(k) - if (Array.isArray(pb[key])) return pb[key].map(toJSON) - else if (pb[key] instanceof Proto) result[key] = pb[key].toJSON() - else if (pb[key] && typeof pb[key] === 'object') { - result[key] = toJSON(pb[key]) - } else if (Buffer.isBuffer(pb[key])) { result[key] = pb[key].toString('hex') } else result[key] = pb[key] - } - return result - } - return toJSON(this) - } - - [Symbol.toPrimitive] () { - return this.toString() - } -} - -function _encode (writer, tag, value) { - if (value === null || value === undefined) { - return - } - let type = 2 - if (typeof value === 'number') { - type = Number.isInteger(value) ? 0 : 1 - } else if (typeof value === 'string') { - value = Buffer.from(value) - } else if (value instanceof Uint8Array) { - // - } else if (value instanceof Proto) { - value = value.toBuffer() - } else if (typeof value === 'object') { - value = encode(value) - } else if (typeof value === 'bigint') { - const tmp = new pb.util.Long() - tmp.unsigned = false - tmp.low = Number(value & 0xffffffffn) - tmp.high = Number((value & 0xffffffff00000000n) >> 32n) - value = tmp - type = 0 - } else { - return - } - const head = (tag << 3) | type - writer.uint32(head) - switch (type) { - case 0: - if (value < 0) { - writer.sint64(value) - } else { - writer.int64(value) - } - break - case 2: - writer.bytes(value) - break - case 1: - writer.double(value) - break - } -} - -export function encode (obj) { - Reflect.setPrototypeOf(obj, null) - const writer = new pb.Writer() - for (const tag of Object.keys(obj).map(Number)) { - const value = obj[tag] - if (Array.isArray(value)) { - for (let v of value) { - _encode(writer, tag, v) - } - } else { - _encode(writer, tag, value) - } - } - return writer.finish() -} - -function long2int (long) { - if (long.high === 0) { - return long.low >>> 0 - } - const bigint = (BigInt(long.high) << 32n) | (BigInt(long.low) & 0xffffffffn) - const int = Number(bigint) - return Number.isSafeInteger(int) ? int : bigint -} - -export function decode (encoded) { - const result = new Proto(encoded) - const reader = new pb.Reader(encoded) - while (reader.pos < reader.len) { - const k = reader.uint32() - const tag = k >> 3 - const type = k & 0b111 - let value, decoded - switch (type) { - case 0: - value = long2int(reader.int64()) - break - case 1: - value = long2int(reader.fixed64()) - break - case 2: - value = Buffer.from(reader.bytes()) - try { - decoded = decode(value) - } catch { } - value = new Proto(value, decoded) - break - case 5: - value = reader.fixed32() - break - default: - return null - } - if (Array.isArray(result[tag])) { - result[tag].push(value) - } else if (Reflect.has(result, tag)) { - result[tag] = [result[tag]] - result[tag].push(value) - } else { - result[tag] = value - } - } - return result -} - -export function decodePb (buffer_data) { - let pb = { - decode, - encode - } - let proto = pb.decode(buffer_data) - let json = {} - let data - // delete proto; - // console.log("小叶子调试",pb.decode(proto[3][1][2][1][1][1])); - // let index = 0 - async function decode2 (proto, json) { - for (let key in proto) { - if (key == 'encoded') { - continue - } - if (proto[key] instanceof Object) { - if (proto[key] instanceof Array) { - json[key] = [] - for (let i = 0; i < proto[key].length; i++) { - json[key].push({}) - decode2(proto[key][i], json[key][i]) - } - } else { - try { - if (pb.decode(proto[key].encoded) == null) { - if (data.length > 3) { - let Prefix = '' - if (data[0] == 0x01 || data[0] == 0x00) { - Prefix = data.toString('hex').slice(0, 2) - data = data.slice(1) - } - let data_json = {} - data_json.Prefix = Prefix - if (data[0] == 0x78 && data[1] == 0x9c) { - let Deflatedata = zlib.unzipSync(data) - // data_json.RawData = proto[key].encoded; - // data_json.DecompressedData =Deflatedata; - // data_json.CompressType = "Deflate" - data_json.txt = Deflatedata.toString() - data_json.tip = - '数据被加密过,使用时请把数据加密回去 deflateSync()' - json[key] = data_json - decode2(proto[key], json[key]) - continue - } else { - json[key] = proto[key].encoded.toString() - decode2(proto[key], json[key]) - continue - } - } - json[key] = proto[key].encoded.toString() - decode2(proto[key], json[key]) - continue - } - json[key] = {} - decode2(proto[key], json[key]) - } catch (error) { - data = proto[key].encoded - if (data.length > 3) { - let Prefix = '' - if (data[0] == 0x01 || data[0] == 0x00) { - Prefix = data.toString('hex').slice(0, 2) - data = data.slice(1) - } - let data_json = {} - data_json.Prefix = Prefix - if (data[0] == 0x78 && data[1] == 0x9c) { - let Deflatedata = zlib.unzipSync(data) - // data_json.RawData = proto[key].encoded; - // data_json.DecompressedData =Deflatedata; - // data_json.CompressType = "Deflate" - data_json.txt = Deflatedata.toString() - data_json.tip = - '数据被加密过,使用时请把数据加密回去 deflateSync()' - json[key] = data_json - decode2(proto[key], json[key]) - continue - } else { - json[key] = proto[key].encoded.toString() - decode2(proto[key], json[key]) - continue - } - } - json[key] = proto[key].encoded.toString() - decode2(proto[key], json[key]) - continue - } - } - } else { - // console.log("小叶子调试",proto[key]); - let value = proto[key] - if (typeof value == 'bigint') { - value = value.toString() - value = Number(value) - } - json[key] = value - } - } - } - decode2(proto, json) - return json -} diff --git a/lib/utils/segment.js b/lib/utils/segment.js deleted file mode 100644 index c1c0ee5..0000000 --- a/lib/utils/segment.js +++ /dev/null @@ -1,373 +0,0 @@ -import { - KarinAtElement, - KarinContactElement, - KarinCustomMusicElement, - KarinDiceElement, - KarinFaceElement, - KarinForwardElement, - KarinImageElement, - KarinJsonElement, - KarinLocationElement, - KarinLongMsgElement, - KarinMusicElement, - KarinNodeElement, - KarinPokeElement, - KarinRecordElement, - KarinReplyElement, - KarinRpsElement, - KarinShareElement, - KarinTextElement, - KarinvideoElement, - KarinXmlElement, -} from '../bot/KarinElement.js' - -export default class Segment { - /** - * 纯文本 - * @param {string} text - 文本内容 - * @returns {KarinTextElement} 纯文本消息 - */ - text (text) { - return new KarinTextElement(text) - } - - /** - * 表情 - * @param {number} id - QQ 表情 ID - * @returns {KarinFaceElement} QQ 表情消息 - */ - face (id) { - return new KarinFaceElement(id) - } - - /** - * 图片 - * @param {string} file - 图片文件名或URL - * @returns {KarinImageElement} 图片消息 - */ - image (file) { - return new KarinImageElement(file) - } - - /** - * 语音 - * @param {string} file - 语音文件名或URL - * @param {boolean} [magic] - 是否魔法语音,默认为 false - * @returns {KarinRecordElement} 语音消息 - */ - record (file, magic = false) { - return new KarinRecordElement(file, magic) - } - - /** - * 短视频 - * @param {string} file - 视频文件名或URL - * @returns {KarinvideoElement} 短视频消息 - */ - video (file) { - return new KarinvideoElement(file) - } - - /** - * @某人 - * @param {string} uid - uid或'all' - * @param {string?} uin - uin - * @returns {KarinAtElement} @某人消息 - */ - at (uid, uin) { - return new KarinAtElement(uid, uin) - } - - /** - * 猜拳魔法表情 - * @returns {KarinRpsElement} 猜拳魔法表情消息 - */ - rps (id) { - return new KarinRpsElement(id) - } - - /** - * 掷骰子魔法表情 - * @returns {KarinDiceElement} 掷骰子魔法表情消息 - */ - dice (id) { - return new KarinDiceElement(id) - } - - /** - * 窗口抖动(戳一戳) - * @returns {{type: string, id:number}} 戳一戳消息 - */ - shake (id) { - return { type: 'shake', id } - } - - /** - * 戳一戳 - * @param {number} poke - 类型 见https://github.com/mamoe/mirai/blob/f5eefae7ecee84d18a66afce3f89b89fe1584b78/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt#L49 - * @param {number} id - ID - * @param {number} strength - 强度 - * @returns {KarinPokeElement} 戳一戳消息 - */ - poke (poke, id, strength = 1) { - return new KarinPokeElement(poke, id, strength) - } - - /** - * 匿名发消息 - * @param {number} [ignore] - 可选,表示无法匿名时是否继续发送 - * @returns {{type: string, ignore: number}} 匿名发消息 - */ - anonymous (ignore) { - return { type: 'anonymous', ignore } - } - - /** - * 链接分享 - * @param {string} url - URL - * @param {string} title - 标题 - * @param {string} [content] - 内容描述 - * @param {string} [image] - 图片 URL - * @returns {KarinShareElement} 链接分享消息 - */ - share (url, title, content, image) { - return new KarinShareElement(url, title, content, image) - } - - /** - * 分享名片 - * @param {string} platform - 类型,'qq' 表示推荐好友,'group' 表示推荐群 - * @param {string} id - 被推荐人的 QQ 号或被推荐群的群号 - * @returns {KarinContactElement} 推荐消息 - */ - contact (platform, id) { - return new KarinContactElement(platform, id) - } - - /** - * 位置 - * @param {number} lat - 纬度 - * @param {number} lon - 经度 - * @param {string?} title - 标题 - * @param {string?} content - 内容描述 - * @returns {KarinLocationElement} 位置消息 - */ - location (lat, lon, title, content = '') { - return new KarinLocationElement(lat, lon, title, content) - } - - /** - * 音乐分享 - * @param {'qq' | '163' | 'xm'} platform - 音乐类型,'qq', '163', 'xm' - * @param {string} id - 歌曲 ID - * @returns {KarinMusicElement} 音乐分享消息 - */ - music (platform, id) { - return new KarinMusicElement(platform, id) - } - - /** - * 自定义音乐分享 - * @param {string} url - 点击后跳转目标 URL - * @param {string} audio - 音乐 URL - * @param {string} title - 标题 - * @param {string} [content] - 内容描述 - * @param {string} [image] - 图片 URL - * @returns {KarinCustomMusicElement} 自定义音乐分享消息 - */ - customMusic (url, audio, title, content, image) { - return new KarinCustomMusicElement(url, audio, title, content, image) - } - - /** - * 回复 - * @param {string} id - 回复时引用的消息 ID - * @returns {KarinReplyElement} 回复消息 - */ - reply (id) { - return new KarinReplyElement(id) - } - - /** - * 合并转发节点 发已有消息id使用 - * @param {string} id - 转发的消息 ID - * @returns {KarinForwardElement} 合并转发节点消息 - */ - forward (id) { - return new KarinForwardElement(id) - } - - /** - * 合并转发自定义节点 - * @param {string} user_id - 发送者 QQ 号 - * @param {string} nickname - 发送者昵称 - * @param {KarinElement[]} content - 消息内容,可以是消息对象数组或字符串 - * @returns {KarinNodeElement} 合并转发自定义节点消息 - */ - node (user_id, nickname, content) { - return new KarinNodeElement(user_id, nickname, content) - } - - /** - * XML 消息 - * @param {string} data - XML 内容 - * @param {string?} id - * @returns {KarinXmlElement} XML 消息 - */ - xml (data, id) { - return new KarinXmlElement(data) - } - - /** - * JSON 消息 - * @param {string} data - JSON 内容 - * @returns {KarinJsonElement} JSON 消息 - */ - json (data) { - data = typeof data === 'string' ? data : JSON.stringify(data) - return new KarinJsonElement(data) - } - - /** - * markdown消息 - * @param {Object} data - markdown消息内容 - * @param {string} [data.content] - 原生markdown内容 - * @param {{ - * key: string, - * values: string[] - * }[]} [data.params] - 模板markdown参数 - * @returns {{type: string, content?: string, custom_template_id?: string, params?:{key:string,values:string[]}[]}} markdown消息 - */ - markdown (data) { - if (typeof data === 'string') { - return { type: 'markdown', content: data } - } else if (data.content) { - return { type: 'markdown', content: data.content } - } else { - return { - type: 'markdown', - custom_template_id: data.custom_template_id, - params: data.params, - } - } - } - - /** - * 构建官方按钮消息段 - * @param {any} data 在原有的segmene.button上支持二维数组 - * @returns {{type: 'rows', data: any}} 官方按钮消息段 - */ - rows (data) { - const rows = [] - if (!Array.isArray(data)) data = [data] - for (const i of data) { - rows.push(this.button(i)) - continue - } - return { type: 'rows', rows } - } - - /** - * 按钮构建 - * @param {{ - * text: string, - * type?: number, - * link?: string, - * data?: string, - * show?: string, - * style?: number, - * enter?: boolean, - * reply?: boolean, - * admin?: boolean, - * list?: string[], - * role?: string[], - * tips?: string - * }[]} data - 包含按钮信息的对象数组,或者单个按钮信息对象。 - * @param {string} data.text - 按钮上的文字。 - * @param {number} [data.type] - 按钮类型:0 跳转按钮,1 回调按钮,2 指令按钮,默认为 2。 - * @param {string} [data.link] - 按钮跳转链接。 - * @param {string} [data.data] - 操作相关的数据 - * @param {string} [data.show] - 按钮点击后显示的文字,不传为text。 - * @param {number} [data.style] - 按钮样式:0 灰色线框,1 蓝色线框。 - * @param {boolean} [data.enter] - 指令按钮可用,点击按钮后直接自动发送 data,默认 false。 - * @param {boolean} [data.reply] - 指令按钮可用,指令是否带引用回复本消息,默认 false。 - * @param {boolean} [data.admin] - 仅管理者可操作。 - * @param {string[]} [data.list] - 有权限的用户 id 的列表。 - * @param {string[]} [data.role] - 有权限的身份组 id 的列表(仅频道可用)。 - * @param {boolean} [data.tips] - 客户端不支持本 action 的时候,弹出的 toast 文案。 - * @returns {{type: 'button', buttons: {id: string, render_data: {label: string, style: number, visited_label: string}, action: {type: number, data: string, unsupport_tips: string, permission: {type: number, specify_user_ids?: string[], specify_role_ids?: string[]}}}}} 按钮消息 - */ - button (data) { - let id = 0 - const buttons = [] - if (!Array.isArray(data)) data = [data] - for (const i of data) { - // 按钮类型:0 跳转按钮,1 回调按钮,2 指令按钮 请开发者注意 这里需要为int类型 - const type = i.link ? 0 : (i.type ?? 2) - - const data = { - id: String(id), - render_data: { - // 按钮上的文字 - label: i.text || i.link, - // 按钮样式:0 灰色线框,1 蓝色线框 - style: i.style ?? 0, - // 点击后按钮的上显示的文字 - visited_label: i.show || i.text || i.link, - }, - action: { - // 设置 0 跳转按钮:http 或 小程序 客户端识别 scheme,设置 1 回调按钮:回调后台接口, data 传给后台,设置 2 指令按钮:自动在输入框插入 @bot data - type, - // 操作相关的数据 - data: i.data || i.link || i.text, - // 客户端不支持本action的时候,弹出的toast文案 - unsupport_tips: i.tips || '.', - // 0 指定用户可操作,1 仅管理者可操作,2 所有人可操作,3 指定身份组可操作(仅频道可用) - permission: { type: 2 }, - }, - } - - // 指令按钮可用,点击按钮后直接自动发送 data,默认 false。支持版本 8983 - if (i.enter) data.action.enter = true - // 指令按钮可用,指令是否带引用回复本消息,默认 false。支持版本 8983 - if (i.reply) data.action.reply = true - // 仅管理者可操作 - if (i.admin) data.action.permission.type = 1 - // 有权限的用户 id 的列表 - if (i.list) { - i.action.permission.type = 0 - data.action.permission.specify_user_ids = i.list - } - // 有权限的身份组 id 的列表(仅频道可用) - if (i.role) { - i.action.permission.type = 3 - data.action.permission.specify_role_ids = i.role - } - - buttons.push(data) - - // 递增 - id++ - } - return { type: 'button', buttons, log: JSON.stringify(data) } - } - - /** - * 构建官方按钮模板消息段 - * @param {string} key - * @param {string} values - * @returns {{key: string, values: string[]}} 官方按钮模板消息段 - */ - params (key, values) { - return { key, values: [values] } - } - - /** - * 构建长消息消息段 - * @param {string} id - * @returns {KarinLongMsgElement} 长消息 - */ - long_msg (id) { - return new KarinLongMsgElement(id) - } -} diff --git a/lib/utils/update.js b/lib/utils/update.js deleted file mode 100644 index 004cf6e..0000000 --- a/lib/utils/update.js +++ /dev/null @@ -1,164 +0,0 @@ -import fs from 'fs' -import exec from './exec.js' - -export default class Update { - constructor () { - this.dir = './plugins' - } - - /** - * 获取插件列表 拥有package.json才会被识别 - * @returns {string[]} - */ - getPlugins () { - const list = [] - const files = fs.readdirSync(this.dir, { withFileTypes: true }) - /** 忽略非文件夹、非karin-plugin-开头的文件夹或/karin-adapter-开头的文件夹 */ - files.forEach(file => { - if (!file.isDirectory()) return - if (!file.name.startsWith('karin-plugin-') && !file.name.startsWith('karin-adapter-')) return - if (!fs.existsSync(`${this.dir}/${file.name}/package.json`)) return - list.push(file.name) - }) - return list - } - - /** - * 更新框架或插件 - * @param {string} path - 插件相对路径 - * @param {string} cmd - 执行命令 - * @param {number} time - 超时时间 默认120s - * @returns {promise<{status: 'ok'|'failed', data: string}>} - */ - async update (path, cmd = 'git pull', time = 120) { - /** 检查一下路径是否存在 */ - if (!fs.existsSync(path)) return { status: 'failed', data: '路径不存在' } - /** 检查是否有.git文件夹 */ - if (!fs.existsSync(`${path}/.git`)) return { status: 'failed', data: '该路径不是一个git仓库' } - - /** 设置超时时间 */ - const timer = setTimeout(() => { return { status: 'failed', data: '执行超时' } }, time * 1000) - - const options = { env: process.env, cwd: path, encoding: 'utf-8' } - /** 记录当前短哈希 */ - const hash = await this.getHash(path) - - /** 执行更新 */ - const { status, error } = await exec(cmd, true, options) - - /** 执行成功 */ - if (status === 'ok') { - clearTimeout(timer) - /** 再次获取短哈希 查看是否有更新 */ - const newHash = await this.getHash(path) - if (hash === newHash) { - const time = await this.getTime(path) - return { - status: 'ok', - data: [ - '\n当前版本已是最新版本', - `Hash: ${hash}`, - `最后更新:${await this.getCommit({ path, count: 1 })}`, - `最后提交时间:${time}`, - ].join('\n'), - } - } - const Commit = await this.getCommit({ path, hash }) - return { - status: 'ok', - data: [ - '\n更新成功', - `当前Hash: ${newHash}`, - `更新日志:\n${Commit}`, - ].join('\n'), - } - } - - const msg = [ - '\n更新失败', - `当前Hash: ${hash}`, - `错误信息:${error?.message?.toString() || error?.stack?.toString() || error?.toString()}`, - '请解决错误后重试或执行【#强制更新】', - ] - return { status: 'failed', data: msg.join('\n') } - } - - /** - * 获取指定仓库最后一次提交时间日期 - * @param {string} path - 插件相对路径 - * @returns {Promise} - */ - async getTime (path) { - const cmd = 'git log -1 --format=%cd --date=format:"%Y-%m-%d %H:%M:%S"' - const data = await exec(cmd, false, { env: process.env, cwd: path, encoding: 'utf-8' }) - if (data.status === 'failed') return '获取时间失败,请重试或更新Git~' - return data.stdout.trim() - } - - /** - * 获取指定仓库最后一次提交哈希值 - * @param {string} path - 插件相对路径 - * @param {boolean} [short] - 是否获取短哈希 默认true - * @returns {Promise} - */ - async getHash (path, short = true) { - const cmd = short ? 'git rev-parse --short HEAD' : 'git rev-parse HEAD' - const data = await exec(cmd, false, { env: process.env, cwd: path, encoding: 'utf-8' }) - if (data.status === 'failed') throw new Error(data.error) - return data.stdout.trim() - } - - /** - * 获取指定仓库的提交记录 - * @param {{ - * path: string, - * count?: number, - * hash?: string - * }} options - 参数 - * @param {string} options.path - 插件相对路径 - * @param {number} [options.count] - 获取日志条数 默认1条 - * @param {string} [options.hash] - 指定HEAD - * @returns {Promise} - */ - async getCommit (options) { - const { path, count = 1, hash, branch } = options - let cmd = `git log -${count} --format="[%ad]%s %n" --date="format:%m-%d %H:%M"` - /** 键入HEAD */ - if (hash) cmd = `git log ${hash}..HEAD --format="[%ad] %s %n" --date="format:%m-%d %H:%M"` - /** 指定分支 */ - if (branch) cmd = `git log -${count} ${branch} --format="[%ad] %s %n" --date="format:%m-%d %H:%M"` - const data = await exec(cmd, false, { env: process.env, cwd: path, encoding: 'utf-8' }) - if (data.status === 'failed') throw new Error(data.error) - return data.stdout.trim() - } - - /** - * 检查插件是否有更新 - * @param {string} path - 插件相对路径 - * @param {number} [time] - 超时时间 默认120s - * @returns {Promise<{status: 'ok'|'failed', data: string|boolean}>} - */ - async checkUpdate (path, time = 120) { - /** 检查一下路径是否存在 */ - if (!fs.existsSync(path)) return { status: 'failed', data: '路径不存在' } - /** 检查是否有.git文件夹 */ - if (!fs.existsSync(`${path}/.git`)) return { status: 'failed', data: '该路径不是一个git仓库' } - - /** 设置超时时间 */ - const timer = setTimeout(() => { return { status: 'failed', data: '执行超时' } }, time * 1000) - - const options = { env: process.env, cwd: path, encoding: 'utf-8' } - /** git fetch origin */ - const { status, error } = await exec('git fetch origin', false, options) - if (status === 'failed') return { status: 'failed', data: error.message } - /** git status -uno */ - const { stdout } = await exec('git status -uno', false, options) - clearTimeout(timer) - /** 检查是否有更新 没更新直接返回 */ - if (stdout.includes('Your branch is up to date with')) return { status: 'ok', data: false } - /** 获取落后几次更新 */ - const count = stdout.match(/Your branch is behind '.*' by (\d+) commits/)?.[1] || 1 - const data = await this.getCommit({ path, count, branch: 'origin' }) - return { status: 'ok', data, count } - } -} diff --git a/lib/utils/updateVersion.js b/lib/utils/updateVersion.js deleted file mode 100644 index 9be456a..0000000 --- a/lib/utils/updateVersion.js +++ /dev/null @@ -1,146 +0,0 @@ -import fs from 'fs' -import { exec } from 'child_process' - -export default class Version { - constructor (path) { - this.path = path - this.packageDir = this.path + '/package.json' - this.CHANGELOGDir = this.path + '/CHANGELOG.md' - this.packageJson = JSON.parse(fs.readFileSync(this.packageDir, 'utf-8')) - this.version = this.packageJson.version - - this.commitsMap = { - release: ['### Releases'], - feat: ['### 新增功能'], - fix: ['### Bug修复'], - docs: ['### 文档更新'], - style: ['### 代码样式修改'], - refactor: ['### 代码重构'], - perf: ['### 性能优化'], - chore: ['### 构建工具相关'], - revert: ['### 回滚'], - others: ['### 其他提交'] - } - } - - async init () { - /** 获取仓库地址 */ - await this.gitUrl() - - /** 取tag对应的head */ - await this.gitTag() - const HEAD = this.HEAD ? `${this.HEAD}..HEAD ` : '' - this.cmd = `git log ${HEAD}--pretty=format:"HEAD: %H=分割=sha: %h=分割=log: %s"` - - this.stdout = await this.exce(this.cmd) - if (!this.stdout) throw new Error('commit为空...') - this.stdout = this.stdout.trim().split('\n') - - const reg = /(fix|feat|docs|style|refactor|perf|release|chore|revert)(:|:)/i - - for (const commit of this.stdout) { - /** 拆分 */ - const [HEAD, sha, log] = commit.split('=分割=').map(item => item.trim().replace(/^(HEAD|sha|log): /, '')) - /** 排除此项 */ - if (log.startsWith('Merge branch')) continue - - const match = log.match(reg) - const type = match ? match[1].toLowerCase() : 'others' - /** 拼接commit */ - const info = `- ${log} ([${sha}](${this.url}/commit/${HEAD}))` - this.commitsMap[type].push(info) - } - - /** 更新版本号 */ - await this.updateVersion() - - /** YYYY-MM-DD */ - const date = new Date().toLocaleDateString().replace(/\//g, '-') - - const text = [`## ${this.version} (${date})`] - - for (const key in this.commitsMap) { - /** 排除空 */ - if (this.commitsMap[key].length === 1) { - delete this.commitsMap[key] - continue - } - - text.push(this.commitsMap[key].join('\n')) - } - - /** 判断是否有CHANGELOG.md文件 */ - if (fs.existsSync(this.CHANGELOGDir)) { - const oldText = fs.readFileSync(this.CHANGELOGDir, 'utf-8') - text.push(oldText) - } - - fs.writeFileSync(this.CHANGELOGDir, text.join('\n\n')) - console.log('更新成功~') - } - - /** - * 获取当前仓库最新的一个标签的HEAD - */ - async gitTag () { - const cmd = 'git tag' - const stdout = await this.exce(cmd) - if (!stdout) return false - const tags = stdout.trim().split('\n') - const lastTag = tags[tags.length - 1] - const cmd2 = `git rev-list -n 1 ${lastTag}` - const HEAD = await this.exce(cmd2) - this.HEAD = HEAD.trim() - } - - /** - * 获取当前仓库的远程地址 - */ - async gitUrl () { - const cmd = 'git remote -v' - const stdout = await this.exce(cmd) - let [url] = stdout.split('\n').map(item => item.split('\t')[1]) - url = url.trim().split(' ')[0] - this.url = url - } - - /** - * 更新版本号 tag - */ - async updateVersion () { - /** 10进1 */ - const versionArr = this.version.split('.').map(item => parseInt(item)) - if (versionArr[2] === 9) { - versionArr[1]++ - versionArr[2] = 0 - } else { - versionArr[2]++ - } - this.version = versionArr.join('.') - this.packageJson.version = this.version - fs.writeFileSync(this.packageDir, JSON.stringify(this.packageJson, null, 2)) - - /** tag */ - try { - await this.exce(`git tag v${this.version}`) - } catch (e) { - console.log('tag已存在') - } - } - - /** - * 封装exce方法 - * @param {string} cmd 执行的命令 - */ - exce (cmd) { - return new Promise((resolve, reject) => { - exec(cmd, { cwd: this.path }, (error, stdout) => { - if (error) { - reject(error) - return - } - resolve(stdout) - }) - }) - } -} diff --git a/package.json b/package.json index 5a3635d..260418b 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,10 @@ { "name": "karin", - "version": "0.0.3", - "private": true, + "version": "0.6.4", + "private": false, "author": "Karin", "type": "module", - "imports": { - "#Karin": "./lib/index.js" - }, - "main": "./lib/index.js", + "main": "./node_modules/node-karin/lib/index.js", "workspaces": [ "plugins/**" ], @@ -28,6 +25,7 @@ "ver": "node lib/tools/updateVersion.js" }, "dependencies": { + "node-karin": "^0.6.5", "@grpc/grpc-js": "1.10.5", "@grpc/proto-loader": "0.7.12", "art-template": "4.13.2", @@ -46,9 +44,17 @@ "yaml": "2.4.1" }, "devDependencies": { - "eslint": "9.4.0", - "neostandard": "^0.7.0", - "sort-package-json": "^2.10.0" + "@types/express": "latest", + "@types/lodash": "latest", + "@types/node": "latest", + "@types/node-schedule": "latest", + "@types/ws": "latest", + "eslint": "latest", + "neostandard": "latest", + "sort-package-json": "latest", + "tsc-alias": "latest", + "tsx": "latest", + "typescript": "latest" }, "engines": { "node": ">=18"